Cloudflareへのデプロイ
このページで分かること
wrangler.jsoncのバインディング(DB、MEDIA、LOADER)とastro.config.mjsの設定、Workerのエントリーポイント、ビルドとデプロイの手順- 速度と負荷のための設定(Targeted Placement、オブジェクトキャッシュ、Workers Cache)と、独自ドメイン、R2の公開、画像変換
- Cloudflare Access・メール・AI Searchの設定、シークレットの保存方法、プレビュー環境の分け方、デプロイ後の確認とトラブルシューティング
このページの目次
このガイドでは、データベースにD1、メディアにR2を使って、EmDashのサイトをCloudflare Workersにデプロイします。EmDashのCloudflareテンプレートから始めるか、既存のAstroサイトに同じ設定を適用します。
前提条件
- Cloudflareのアカウント
- プロジェクトの依存パッケージがインストールされていること
- WranglerがCloudflareで認証されていること(
pnpm wrangler login)
バインディングを設定する
Cloudflareテンプレートには、Workerのエントリーポイント一式と、名前付きのD1とR2のバインディングが含まれています。最初のデプロイで、設定した名前のリソースがまだ存在しない場合、Wranglerがそのリソースを作成します。wrangler.jsonc の名前はそのまま残します。2回目以降のデプロイでは、Wranglerが同じリソースにつなぎ直します。
テンプレートは次のバインディングを使います。
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-emdash-site",
"main": "./src/worker.ts",
"compatibility_date": "2026-02-24",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "my-emdash-site",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "my-emdash-media",
},
],
"worker_loaders": [{ "binding": "LOADER" }],
"triggers": { "crons": ["* * * * *"] },
}
DB、MEDIA、LOADER の名前は、EmDashのアダプターの設定と一致している必要があります。Cron Triggerは、予約公開、プラグインのタスク、バックアップ、メンテナンスを実行します。サイトでサンドボックス型プラグインを使う場合は、プラグインサンドボックスを参照してください。
やさしい解説
WordPressでは、データベース(MySQL)とアップロードしたファイルの置き場所は、サーバーに最初から用意されているのが一般的です。Cloudflare Workersでは、データベースのD1とファイル置き場のR2を別々のサービスとして用意し、wrangler.jsonc の「バインディング」でWorkerに結び付けます。DB や MEDIA はその結び付けに付ける名前で、astro.config.mjs の d1({ binding: "DB" }) などと同じ名前にします。Cron Triggerは1分ごとにWorkerを起こす設定で、予約公開などはこれで動きます。
EmDashを設定する
次のAstroの設定は、D1とR2のバインディングを使います。
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import emdash from "emdash/astro";
import { d1, r2, sandbox } from "@emdash-cms/cloudflare";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
react(), // Required — the admin UI is a React app
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
sandboxRunner: sandbox(),
}),
],
});
サイトでマーケットプレイス、レジストリ、sandboxed のプラグインを使わない場合は、sandboxRunner と LOADER のバインディングを省略します。
Workerのエントリーポイントを追加する
Workerのエントリーポイントは、AstroをCron Triggerにつなぎ、プラグインブリッジをエクスポートします。
import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";
export { PluginBridge };
export default {
...handler,
scheduled: createScheduledHandler(),
} satisfies ExportedHandler;
PluginBridge のエクスポートは、サンドボックス型プラグインがインストールされていなくても害はありません。同じプロジェクトで後からプラグインを有効にする可能性がある場合は、残しておきます。
一般的なメンテナンスを1分ごと以外の間隔で実行するには、同じCron式を createScheduledHandler({ generalCron: "..." }) と triggers.crons の両方に渡します。両者が異なる場合、ハンドラーはログを出力し、想定外のトリガーを無視します。
ビルドとデプロイ
サイトを一度ビルドしてデプロイし、名前付きのD1データベースとR2バケットをWranglerに用意させます。Wranglerは、pnpm wrangler login で作成されたローカルのログイン情報を使います。
pnpm build
pnpm wrangler deploy
マイグレーションのモードが既定の auto の場合、デプロイしたWorkerが最初のリクエストを受け取ったときに、EmDashが未適用のコアマイグレーションを適用します。新しいコードがリクエストを受け取る前にデプロイのパイプラインでマイグレーションを適用する必要がある場合や、マイグレーションを調べる・確認する・復旧する必要がある場合は、コアDBマイグレーションを使います。
データベースが空(コレクションがない)で、セットアップウィザードが完了していない場合、EmDashは最初の起動時にシードファイルも適用します。シードはビルド時に .emdash/seed.json、package.json#emdash.seed に書かれたパス、seed/seed.json のうち最初に見つかったものから読み込まれ、バンドルに埋め込まれます。どれもない場合は、組み込みの既定のシードが使われます。既存のデータベースに対するその後のデプロイでは、そのコンテンツには手を加えません。
すでにデプロイしたサイトのスキーマやコンテンツモデルを変更するには、運用中サイトのスキーマ変更を参照してください。
やさしい解説
WordPressでは、新しいバージョンにしたときのデータベースの更新は、管理画面で「データベースを更新」を押すなどして実行します。EmDashでは、既定の設定(auto)の場合、デプロイ後の最初のアクセスで、必要なデータベースの更新(コアマイグレーション)が自動で適用されます。また、データベースが空の最初のデプロイでは、シードファイルに書かれたコレクションなどの初期構成が自動で作られます。すでにコンテンツがあるデータベースでは、シードは適用されません。
WorkerをD1の近くに配置する
Cloudflareは、既定では訪問者の近くでWorkerを実行します。EmDashのサーバーで描画するリクエストはD1との往復を何度も発生させるため、Targeted Placementを使ってD1のプライマリーの近くでWorkerを実行し、これらのリクエストを速くします。
Wranglerは、placement.mode: "targeted" と、region、host、hostname のうちちょうど1つのセレクターを受け付けます。D1のプライマリーの場所を対象にする値を選び、その結果の placement オブジェクトを wrangler.jsonc に追加します。Targeted Placementと同時にD1のリードレプリカを有効にしないでください。EmDashの session の設定は既定の "disabled" のままにして、読み込みと書き込みが近くのプライマリーを使うようにします。
オブジェクトキャッシュ
D1の読み込みの負荷を減らすには、コンテンツと設定を取得した結果をCloudflare KVにキャッシュします。読み込みは、リクエストのたびにデータベースに問い合わせる代わりに、KVから返されます。
import { d1, r2, kvCache } from "@emdash-cms/cloudflare";
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
objectCache: kvCache({ binding: "CACHE" }),
}),
KVの準備、オプション、無効化の動作については、オブジェクトキャッシュを参照してください。
Workers Cache
CloudflareのWorkers Cacheは、Workerの前段にエッジキャッシュを置きます。条件に合うリクエストには、Workerをまったく実行せずに応答が返されます。
有効にする
-
Astroのキャッシュプロバイダー(Cloudflare用)を使い、ルートのルールと
Astro.cacheがキャッシュのヘッダーを設定し、無効化にcache.purge()が使われるようにします。astro.config.mjs import { cacheCloudflare } from "@astrojs/cloudflare/cache"; export default defineConfig({ adapter: cloudflare(), cache: { provider: cacheCloudflare(), }, routeRules: { "/": { maxAge: 300, swr: 86400 }, // Other public routes can use different cache lifetimes. }, });@astrojs/cloudflareアダプターはcacheCloudflare()を検出し、生成するデプロイの設定でWorkers Cacheを有効にします。 -
キャッシュした応答は、WorkerのコードからプラットフォームのAPIを使って削除(パージ)します。この呼び出しには、CloudflareのREST APIの認証情報は必要ありません。
import { cache } from "cloudflare:workers"; await cache.purge({ purgeEverything: true }); // Or purge selected tags: await cache.purge({ tags: ["posts"] });
EmDashの管理画面とAPIの応答は、もともと Cache-Control: private, no-store を送るため、保存されることはありません。公開ページは、Cache-Control/routeRules/Astro.cache で自分のキャッシュを制御します。
有効にする前に知っておくことが2つあります。
Cache-Controlヘッダーがない応答もキャッシュされます。 Workers CacheはRFC 9111のヒューリスティックな鮮度を適用します。ヘッダーがまったくない200の応答は、2時間キャッシュされます。独自のルートにはすべて、明示的なCache-Controlを付けてください(セッションに依存するものにはprivate, no-storeを使います)。- キャッシュされたページは、ログイン中の編集者にも共有されます。 キャッシュはWorkerより前で動くため、リクエストのCookieに基づいてキャッシュを迂回できません。ログイン中の編集者は、キャッシュの有効期限が切れるまで、公開ページのキャッシュされた匿名向けの版(ビジュアル編集のツールバーがない版)を受け取ることがあります。編集者向けに描画された応答そのものは保存されない(
private, no-storeが付いている)ため、逆方向に情報が漏れることはありません。
@emdash-cms/cloudflare の cloudflareCache() との違い
| 推奨:Workers Caching | 旧来:cloudflareCache() |
|
|---|---|---|
| 設定 | "cache": { "enabled": true } + @astrojs/cloudflare/cache の cacheCloudflare() |
@emdash-cms/cloudflare の cache: { provider: cloudflareCache() } |
| 保存先 | プラットフォームのWorkers Caching | Cache API(caches.open/put/match) |
| 削除(パージ) | cloudflare:workers の cache.purge() |
ゾーンのREST API POST /zones/{id}/purge_cache |
| シークレット | 削除には不要 | CF_ZONE_ID + CF_CACHE_PURGE_TOKEN |
新しいサイトでは推奨の方法を使います。cloudflareCache() は、そのCache APIの動作にすでに依存している場合にだけ残します。
また、どちらもオブジェクトキャッシュ(objectCache: kvCache({ binding: "CACHE" }))と混同しないでください。オブジェクトキャッシュはデータベースを取得した結果をKVにキャッシュするもので、Workerの下にある別の層です。
やさしい解説
このページには3種類のキャッシュが出てきます。WordPressでいえば、ページ全体を保存するページキャッシュと、データベースの取得結果を保存するオブジェクトキャッシュの違いに近いものです。Workers Cacheは、Workerより前でページの応答そのものを返す仕組みです。cloudflareCache() は同じ目的の旧来の方法です。オブジェクトキャッシュ(kvCache)は、Workerの中でデータベースの取得結果をKVに保存する仕組みです。Workers Cacheを使う場合は、ログイン中の編集者にもキャッシュされたページが表示されることがある点に注意が必要です。
独自ドメイン
最初のデプロイでは workers.dev のURLが割り当てられます。独自ドメインは、Workerと同じアカウントでCloudflareが管理している、有効なドメインである必要があります。Workerが workers.dev のURLで正常に応答することを確かめてから、本番のドメインをWranglerのルートとして追加します。
{
"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}
もう一度デプロイし、両方のアドレスを確かめます。DNSを試している間も workers.dev のアドレスを使えるようにしておくと、ルーティングの問題とアプリケーションの問題を区別しやすくなります。
R2の公開アクセス
既定では、メディアはEmDashの認証付きのメディアルートを通して配信されます。バケットに公開用の独自ドメインがある場合は、そのオリジンを publicUrl に設定すると、生成されるメディアのURLにそのオリジンが使われます。
storage: r2({
binding: "MEDIA",
publicUrl: "https://media.example.com",
}),
バケットの公開アクセスは、メディアだけでなく、アクセスできるすべてのオブジェクトに適用されます。自動のJSONバックアップは同じストレージの backups/ プレフィックスを使うため、そのプレフィックスを公開ドメインから見えるようにしないでください。安全な境界については、メディアストレージの選択で説明しています。
画像変換
EmDashは、Cloudflareの IMAGES バインディングを通して、R2のメディアをWorkerの中でリサイズし、再エンコードします。emdash/ui の Image コンポーネントと、リッチテキスト内の画像は、どちらもCloudflareアダプターの下でEmDashが設置する画像エンドポイントを通して描画されます。内部の /_emdash/api/media/file/… ルートにあるメディアの場合、このエンドポイントはHTTPで取得せず、元のバイト列をR2のバインディングから直接読み込みます。そのため、この変換はCloudflare Accessの背後でも、global_fetch_strictly_public を使っていても動作し続けます。バケットのURLから配信されるメディア(R2の公開アクセスを参照)の場合は、代わりにアダプター自身の変換エンドポイントが使われ、変換の前にHTTPでファイルを取得します。
バインディングを宣言する必要はありません。@astrojs/cloudflare は、Workers Cachingのために cache を追加するのと同じ方法で、astro build の間に生成するWorkerの設定にこのバインディングを追加します。追加されるのは、実行時の画像サービスが cloudflare-binding の場合です。つまり、imageService が未設定の場合、この文字列そのものの場合、{ runtime: "cloudflare-binding" } の場合です。それ以外の値("passthrough"、"compile"、"cloudflare"、"custom")では、バインディングは追加されません。自分の wrangler.jsonc に書いておくと、意図が明確になります。
{
"images": {
"binding": "IMAGES",
},
}
デプロイで実際に何が使われるかを確かめるには、wrangler.jsonc ではなく、生成された設定を読みます。ビルドは .wrangler/deploy/config.json を書き出し、このファイルが wrangler deploy にマージ済みのファイル(既定では dist/server/wrangler.json)を指定します。そこに images の項目があるかを確認します。
Cloudflareは、これらの変換をImages transformationsとして課金します。元の画像とパラメーターの一意な組み合わせごとに、暦月あたり1回課金され、その月の中での繰り返しのリクエストは無料です。たとえば、500枚の元画像があるサイトで、すべての画像についてサムネイルのサイズとヒーロー画像のサイズを1つずつリクエストする場合、その2つのパラメーターの組み合わせで、その月は1,000件の変換画像として数えられます。ImagesのFreeプランには、月あたり5,000件の一意な変換が含まれます。この上限を超えると、キャッシュされた変換は引き続き配信されますが、新しい変換は 9422 エラーを返し、画像のリクエストは失敗します。
Cloudflare Accessによる認証
Cloudflare Accessを使うと、パスキー認証の代わりに、Accessアプリケーションに接続したIDプロバイダーで認証できます。audienceの値は秘密にする実行時の設定です。環境変数の名前を指定して、値を astro.config.mjs に書かないようにします。
import { access } from "@emdash-cms/cloudflare";
emdash({
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audienceEnvVar: "CF_ACCESS_AUDIENCE",
roleMapping: {
Admins: 50,
Editors: 40,
},
}),
}),
CF_ACCESS_AUDIENCE は pnpm wrangler secret put CF_ACCESS_AUDIENCE で設定します。ユーザーの作成、既定のロール、ロールの同期については、認証のガイドで説明しています。
メール
本番のWorkerには、既定のメール配信サービスがありません。メールのプラグインが有効になるまで、マジックリンクによるサインイン、チームへの招待、コメントの通知は Email is not configured を返します。
Cloudflareのメールプラグインは、send_email バインディングを使います。まず、Cloudflare Email Sendingで送信元のドメインを登録して確認します。Cloudflareは、Fromのアドレスが承認された送信元ではないメッセージを拒否します。
バインディングを追加し、プロバイダーを登録します。
{
"send_email": [{ "name": "EMAIL" }],
}
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";
emdash({
plugins: [
cloudflareEmail({
from: { email: "cms@mails.example.com", name: "My Site CMS" },
replyTo: "hello@example.com",
}),
],
}),
デプロイ後、「Extensions」 でプラグインを有効にし、「設定」→「メール」 でそのプラグインを選択します。送信元が承認され、バインディングが存在するまで、送信は失敗します。
binding オプションで別の名前を指定しない限り、プラグインは EMAIL という名前のバインディングを使います。有効なメールプロバイダーがこれだけの場合、EmDashは自動でこれを選択します。複数のプロバイダーが有効な場合は、「設定」→「メール」 でCloudflareのプロバイダーを選択します。省略可能な replyTo のアドレスは、承認済みのFromアドレスを変えずに返信を受け取ります。
Cloudflare AI Search
AI Searchプラグインには、ネイティブ型プラグインとしての登録と、ai_search_namespaces バインディングの両方が必要です。両方をデプロイした後、管理画面で 「Cloudflare AI Search」 を開き、コレクションを選び、「Sync All Content」 を実行します。最初の同期は、プラグインを有効にする前に公開されたコンテンツをインデックスに登録します。その後の変更は、フックが同期し続けます。
import { aiSearch } from "@emdash-cms/cloudflare/plugins";
emdash({
plugins: [aiSearch()],
}),
{
"ai_search_namespaces": [{ "binding": "AI_SEARCH", "namespace": "default" }],
}
サイトから検索のルートを公開します。
export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
レイアウトに検索のインターフェースを追加します。triggerスロットには、サイトのデザインに合ったボタンを入れられます。
---
import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
---
<AISearchSnippet apiUrl="/api/ai-search" placeholder="Search content">
<button slot="trigger" type="button">Search</button>
</AISearchSnippet>
Workerのシークレット
秘密の値は pnpm wrangler secret put <NAME> で保存します。wrangler.jsonc に書いたり、ビルド時の import.meta.env の値から読み込んだりしないでください。
EMDASH_ENCRYPTION_KEY は、現時点ではプラグインの秘密情報も、その他の保存データも暗号化しません。設定されている場合、EmDashは起動時にその形式を確認します。形式が正しくない値は運用者向けのログメッセージを出力しますが、サイトはリクエストの処理を続けます。プラグインの秘密情報は、データベースに平文のまま残ります。
EmDashは、実行時に process.env から秘密情報を読み込みます。Workerのコードは、cloudflare:workers からインポートした env からバインディングを読み込みます。秘密情報を import.meta.env 経由で読み込まないでください。Viteはこれらの値をビルド時に置き換えるため、サーバーのバンドルに書き込まれることがあります。
プレビューのHMACシークレットと、コメント投稿者のIPのソルトは、実行時の上書きを指定しない限り、生成されてデータベースに保存されます。正確な変数名、保存場所、ローテーションの影響は、シークレットとキーの管理に一覧があります。
やさしい解説
WordPressでは、データベースのパスワードなどの秘密の値を wp-config.php に書くことが一般的です。Cloudflare Workersでは、秘密の値を設定ファイル(wrangler.jsonc)には書かず、pnpm wrangler secret put 名前 でCloudflareに直接登録します。コードの中で import.meta.env から秘密の値を読むと、ビルド時に値がそのまま埋め込まれてしまうことがあるため、使わないようにします。
プレビューのデプロイ
Wranglerの名前付き環境は、バインディングを引き継ぎません。プレビュー用のリソースを別に作成し、ビルドの前に preview 環境に書き込みます。
pnpm wrangler d1 create my-emdash-site-preview \
--binding DB --env preview --update-config
pnpm wrangler r2 bucket create my-emdash-media-preview \
--binding MEDIA --env preview --update-config
プレビュー環境には、プレビューのWorkerが使うすべてのバインディングを繰り返し書く必要があります。Wranglerがリソースの識別子を書き込んだ後、中心となるD1、R2、サンドボックスのバインディングは次の形になります。
{
"env": {
"preview": {
"d1_databases": [
{
"binding": "DB",
"database_name": "my-emdash-site-preview",
"database_id": "00000000-0000-0000-0000-000000000000",
},
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "my-emdash-media-preview",
},
],
"worker_loaders": [{ "binding": "LOADER" }],
},
},
}
Wranglerが書き込んだプレビュー用のUUIDを使います。プレビューでKV、AI Search、メールなどの機能を使う場合は、省略可能なそれらのバインディングも繰り返し書きます。プレビュー専用のシークレットは pnpm wrangler secret put <NAME> --env preview で追加します。
プレビュー環境をビルドしてデプロイします。最初のリクエストで、既定の auto モードにより未適用のコアマイグレーションが適用されます。
pnpm build
pnpm wrangler deploy --env preview
共有する前に、プレビューのURL、管理画面へのサインイン、メディアのアップロード、省略可能なバインディングを確かめます。プレビューのバインディングを、本番のデータベースやバケットに向けないでください。
やさしい解説
WordPressで本番サイトとは別にステージング環境を用意するのと同じように、Cloudflareでもプレビュー用の環境を作れます。このとき、プレビュー用のD1データベースとR2バケットは本番とは別に作成します。wrangler.jsonc の env.preview には、本番と同じバインディングをもう一度すべて書く必要があります。本番のデータベースをプレビューから使うと、本番のコンテンツを書き換えてしまうおそれがあります。
デプロイを確かめる
デプロイ後、公開ページを1つリクエストし、/_emdash/admin にサインインし、テスト用のメディアファイルをアップロードして取得し、pnpm wrangler tail に予約処理のハンドラーが表示されることを確認します。
トラブルシューティング
「D1 binding not found」
wrangler.jsonc のバインディング名が、データベースの設定と一致しているかを確かめます。
// Must match: d1({ binding: "DB" })
"binding": "DB"
「R2 binding not found」
R2バケットが正しくバインドされているかを確認します。
// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"
マイグレーションのエラー
スキーマのエラーが表示される場合は、Workerのログを追跡し(wrangler tail)、エラーを再現して原因のメッセージを記録します。そのうえで、その出力を添えてissueを登録します。