Cloudflareへのデプロイ

このページ

Cloudflare Workersは、EmDashに対して高速でグローバルに分散されたランタイムを提供します。このガイドでは、データベースにD1、メディアストレージにR2を使用したデプロイについて説明します。

前提条件

  • Cloudflareアカウント
  • Wrangler CLIがインストール済み(npm install -g wrangler
  • Cloudflareで認証済み(wrangler login

バインディングの設定

本番用のD1データベースとR2バケットをプロビジョニングし、プロジェクトルートにwrangler.jsoncを作成して、それらの不変IDと名前のバインディングを設定します。データベースのプロビジョニングは、EmDashのスキーママイグレーションの適用とは別です。

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "my-emdash-site",
	"compatibility_date": "2025-01-15",
	"compatibility_flags": ["nodejs_compat"],

	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "emdash-db",
			"database_id": "00000000-0000-0000-0000-000000000000",
		},
	],

	"r2_buckets": [
		{
			"binding": "MEDIA",
			"bucket_name": "emdash-media",
		},
	],
}

これらはあなたが自分で設定するバインディングです。@astrojs/cloudflareアダプターは、デプロイされたWorker設定を生成する際に独自のバインディングを追加します。その1つが、メディア変換で使用されるIMAGESバインディングです — 画像変換を参照してください。

サンドボックス化されたプラグイン — マーケットプレイスのインストールとsandboxed: []以下のプラグイン — にはworker_loadersバインディングとPluginBridgeをエクスポートする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 } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // 必須 — 管理UIはReactアプリです
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

マイグレーションとデプロイ

ランタイムマイグレーションはデフォルトで自動のままです。デプロイメント管理のマイグレーションの場合、Workerをビルドし、アカウントとデータベースのUUIDを使用してプロビジョニングされたD1ターゲットを検査します。

pnpm build
pnpm exec emdash migrate --status --json \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID"

報告されたターゲットフィンガープリントを確認・記録した後、マイグレーションを適用し、同じビルドをデプロイします。

pnpm exec emdash migrate \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID" \
  --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy

マイグレーションジョブにはD1 Edit権限を持つCLOUDFLARE_API_TOKENが必要です。アカウントとデータベースのUUIDでジョブをシリアライズしてください。プロビジョニング、CI並行性、ランタイムモード、リカバリガイダンスについては、コアデータベースマイグレーションの管理を参照してください。

データベースが空(コレクションなし)で、セットアップウィザードが完了していない場合、EmDashは初回起動時にシードファイルも適用します。シードはビルド時に.emdash/seed.jsonpackage.json#emdash.seedのパス、またはseed/seed.jsonから読み取られ — 最初に見つかったものが使用され — バンドルにインライン化されます。どれも存在しない場合、組み込みのデフォルトシードが使用されます。既存のデータベースに対する後続のデプロイはそのコンテンツをそのままにします。

すでにデプロイされたサイトのスキーマやコンテンツモデルを変更するには、デプロイ済みサイトの進化を参照してください。

スケジュールタスク

Cloudflareは、1つのCron Triggerからスケジュール公開、プラグインタスク、一般メンテナンスを実行します。

標準のWorkerエントリポイントを使用します:

import handler, {
	createScheduledHandler,
	PluginBridge,
} from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

export default {
	...handler,
	scheduled: createScheduledHandler(),
} satisfies ExportedHandler;

wrangler.jsoncで一般メンテナンス用のCron Triggerを設定します:

{
	"triggers": {
		"crons": ["* * * * *"],
	},
}

異なる一般メンテナンススケジュールを使用するには、createScheduledHandler()generalCronを設定し、wrangler.jsoncで同じ式を使用します。

デプロイ

Cloudflare Workersにデプロイします:

wrangler deploy

サイトはhttps://my-emdash-site.<your-subdomain>.workers.devで公開されます。

リードレプリカ

グローバルに分散されたサイトの場合、D1リードレプリケーションを有効にして、常にプライマリデータベースにアクセスする代わりに、読み取りクエリを近くのレプリカにルーティングします。これにより、プライマリリージョンから離れた訪問者のレイテンシが大幅に削減されます。

emdash({
	database: d1({
		binding: "DB",
		session: "auto",
	}),
	storage: r2({ binding: "MEDIA" }),
}),

Cloudflareダッシュボードまたは REST API を通じて、D1データベース自体のリードレプリケーションも有効にする必要があります。

セッションモードとブックマークベースの一貫性の仕組みについては、データベースオプション — リードレプリカを参照してください。

オブジェクトキャッシュ

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を実行せずに提供されます。

有効化

  1. wrangler.jsoncでプラットフォームキャッシュを有効にします:
{
	"cache": {
		"enabled": true,
	},
}
  1. AstroのCloudflareキャッシュプロバイダーを使用して、ルートルール / Astro.cacheが正しいヘッダーを設定し、無効化がネイティブのcache.purge()を使用するようにします:
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
	adapter: cloudflare(),
	cache: {
		provider: cacheCloudflare(),
	},
	routeRules: {
		"/": { maxAge: 300, swr: 86400 },
		// …
	},
});

cacheCloudflare()を使用すると、@astrojs/cloudflareアダプターは、生成されたWrangler設定に"cache": { "enabled": true }がない場合にそれを注入します — 自分のwrangler.jsoncに明示的にリストすることで意図が明確になります。

  1. プラットフォームAPIを使用してWorkerからパージします(Cloudflare REST認証情報不要):
import { cache } from "cloudflare:workers";

await cache.purge({ purgeEverything: true });
// または: await cache.purge({ tags: ["posts"] });

EmDashの管理画面とAPIのレスポンスはすでにCache-Control: private, no-storeを送信しており、キャッシュされることはありません。パブリックページはCache-Control / routeRules / Astro.cacheを通じて独自のキャッシュを制御します。

有効化前に知っておくべき2つのこと:

  1. Cache-Controlヘッダーのないレスポンスもキャッシュされます。 Workers CacheはRFC 9111ヒューリスティックフレッシュネスを適用します — ヘッダーのない200は2時間キャッシュされます。すべてのカスタムルートに明示的なCache-Controlを設定してください(セッション依存のものにはprivate, no-storeを使用)。
  2. キャッシュされたページはログイン中のエディターと共有されます。 キャッシュはWorkerの前に実行されるため、リクエストCookieに基づいてバイパスすることができません。ログイン中のエディターは、エントリが期限切れになるまで、パブリックページのキャッシュされた匿名バリアント — ビジュアル編集ツールバーなし — を受け取る可能性があります。エディターがレンダリングしたレスポンス自体はキャッシュされません(private, no-storeが付いています)ので、逆方向には何も漏れません。

@emdash-cms/cloudflarecloudflareCache()とは異なります

推奨: Workers Cachingレガシー: cloudflareCache()
設定"cache": { "enabled": true } + @astrojs/cloudflare/cachecacheCloudflare()@emdash-cms/cloudflarecache: { provider: cloudflareCache() }
ストレージプラットフォームWorkers CachingCache API (caches.open / put / match)
パージcloudflare:workerscache.purge()Zone REST POST /zones/{id}/purge_cache
シークレットパージには不要CF_ZONE_ID + CF_CACHE_PURGE_TOKEN

新しいサイトには推奨パスを使用してください。cloudflareCache()は、そのCache APIの動作にすでに依存している場合にのみ保持してください。

また、どちらもオブジェクトキャッシュobjectCache: kvCache({ binding: "CACHE" }))と混同しないでください。これはデータベースクエリの結果をKVにキャッシュするもので、Worker配下の別のレイヤーです。

カスタムドメイン

Cloudflareダッシュボードでカスタムドメインを追加します:

  1. Workers & Pages > あなたのworkerに移動
  2. Custom Domains > Add Custom Domainをクリック
  3. ドメインを入力し、DNS設定の指示に従います

パブリックR2アクセス

R2から直接メディアを提供するには(パフォーマンスのために推奨):

  1. Cloudflareダッシュボードで、R2 > あなたのバケットに移動
  2. Settings > Public accessをクリック
  3. パブリックアクセスを有効にし、パブリックURLをメモします
  4. ストレージ設定を更新します:
storage: r2({
  binding: "MEDIA",
  publicUrl: "https://pub-xxx.r2.dev"
}),

画像変換

EmDashは、CloudflareのIMAGESバインディングを通じて、Worker内でR2メディアのリサイズと再エンコードを行います。emdash/uiImageコンポーネントとリッチテキスト内の画像は、EmDashがCloudflareアダプターの下にインストールする画像エンドポイントを通じてレンダリングされます。内部ルート/_emdash/api/media/file/…のメディアの場合、そのエンドポイントはHTTPフェッチなしでR2バインディングからソースバイトを直接読み取ります。これらの変換はCloudflare Accessの背後やglobal_fetch_strictly_publicでも動作し続けます。バケットURLから提供されるメディア — パブリックR2アクセスを参照 — は、代わりにアダプター独自の変換エンドポイントを使用し、変換前にHTTP経由でファイルをフェッチします。

バインディングを宣言する必要はありません。@astrojs/cloudflareastro build中に生成するWorker設定にそれを追加します。Workers Cachingのcacheを追加するのと同じ方法です。ランタイム画像サービスが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回課金され、その月内の繰り返しリクエストは無料です。Images無料プランは月あたり5,000のユニーク変換をカバーします。その制限を超えると、キャッシュされた変換は引き続き提供されますが、新しい変換は9422エラーを返し、画像リクエストは失敗します。

Cloudflare Access認証

組織がCloudflare Accessを使用している場合、パスキーの代わりに認証プロバイダーとして使用でき、既存のIDプロバイダーを通じてシングルサインオンを提供します。以下の設定で有効にします:

emdash({
  database: d1({ binding: "DB" }),
  storage: r2({ binding: "MEDIA" }),
  auth: access({
    teamDomain: "myteam.cloudflareaccess.com",
    audience: "your-app-audience-tag",
    roleMapping: {
      "Admins": 50,
      "Editors": 40,
    },
  }),
}),

完全な設定オプションについては、認証ガイドを参照してください。

AI Searchプラグインは、公開されたEmDashコンテンツをインデックスし、サイトにスマート検索インターフェースを追加します。

  1. EmDashに渡されるplugins配列にプラグインを登録します:

    import { aiSearch } from "@emdash-cms/cloudflare/plugins";
    
    // ...
    plugins: [
    	formsPlugin(),
    	aiSearch(),
    ],
  2. Worker設定にAI Searchネームスペースバインディングを追加します:

    {
    	"ai_search_namespaces": [
    		{
    			"binding": "AI_SEARCH",
    			"namespace": "default",
    		},
    	],
    }
  3. 検索インターフェースが使用する検索エンドポイントを作成します:

    export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
  4. サイトレイアウトに検索インターフェースを追加します。トリガースロットには、サイトのデザインに合う任意のボタンを含めることができます:

    ---
    import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
    ---
    
    <AISearchSnippet apiUrl="/api/ai-search" placeholder="検索...">
    	<button slot="trigger" type="button">検索</button>
    </AISearchSnippet>
  5. サイトをデプロイします:

    pnpm exec wrangler deploy
  6. EmDash管理パネルでCloudflare AI Searchを開き、インデックスするコレクションを選択し、Sync All Contentをクリックします。

    この初回同期は必須です:プラグインのコンテンツフックは有効化後に作成または更新されたコンテンツに対してのみ発火するため、それ以前に公開されたものは完全同期を実行するまでインデックスから欠落したままになります。

セットアップ後に公開または更新されたコンテンツは自動的に同期が維持されます。同じページでインデックスの進捗が表示されます。

メール

Workersでは、唯一の組み込みemail:deliverハンドラーは開発コンソールスタブなので、メール依存のフロー — マジックリンクログイン、チーム招待、コメント通知 — は本番環境で**「Email is not configured」**で失敗します。cloudflareEmail()プラグインは、外部APIキーなしで、ネイティブのsend_email Workerバインディングを使用してCloudflare Email Sendingを通じて実際のメールを配信します。

1. 送信ドメインのオンボード

CloudflareダッシュボードでEmailに移動し、送信元のドメイン(またはアドレス)を確認します。Email Sendingは未確認の送信者からのメッセージを拒否します。

2. バインディングの追加

wrangler.jsoncsend_emailバインディングを宣言します:

{
	"send_email": [{ "name": "EMAIL" }],
}

3. プロバイダーの登録

emdash()インテグレーションにプラグインを追加します:

import { d1, r2 } from "@emdash-cms/cloudflare";
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	database: d1({ binding: "DB" }),
	storage: r2({ binding: "MEDIA" }),
	plugins: [
		cloudflareEmail({
			from: { email: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]", // オプション
			binding: "EMAIL", // オプション、デフォルトは "EMAIL"
		}),
	],
}),

4. アクティベートと選択

デプロイし、Admin → Extensionsでプラグインをアクティベートし、Settings → Emailでプロバイダーとして選択します。

オプション

オプションデフォルト説明
fromstring | { email, name? }—(必須)Email Sending用にオンボードされたドメインの送信者アドレス。
replyTostringオプションのReply-To、fromがno-replyサブドメインアドレスの場合に便利。
bindingstring"EMAIL"wrangler.jsoncsend_emailバインディング名。

環境変数

推奨: 暗号化キー

EMDASH_ENCRYPTION_KEYは、プラグインシークレットを保存時に暗号化するためのキーです(webhookトークン、Turnstileキーなど)。キーは起動時に検証され、プラグインシークレット暗号化は有効化されると使用されます。すべてのデプロイメントで設定して、後の設定変更なしでシークレットが保護されるようにしてください。

キーはあなたが提供し、データベースに保存されることはありません。暗号化されたciphertextのみが保存されます。キーを紛失すると、それで暗号化されたすべてのシークレットが失われます。

以下のコマンドでキーを生成し、Workerシークレットとして保存します:

npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY

オプション: 安定値オーバーライド

EmDashはプレビューHMACシークレットとコメンターIPハッシュソルトを自動生成し、初回使用時にデータベースに永続化します。以下の環境変数は、値を自分でピン留めする必要がある場合のオーバーライドです — 例えば、別のプロセスのプレビューWorkerがメインサイトとシークレットを共有する必要がある場合。

変数目的
EMDASH_PREVIEW_SECRET自動生成されたプレビューHMACシークレットのオーバーライド。
EMDASH_IP_SALT自動生成されたコメンターIPハッシュソルトのオーバーライド。
EMDASH_AUTH_SECRETオプション。設定されている場合、IPソルトソースとして使用されます(EMDASH_IP_SALTも設定されている場合はそちらが優先)。すでにそれに依存しているインストールのコメンターIPハッシュを安定させます。新しいデプロイメントでは未設定のままにしてください。

設定で環境変数にアクセスするには、import.meta.envまたはCloudflareのenvバインディングを使用します。

EmDashが使用するすべてのシークレットの完全なインベントリ — ストレージの場所、ローテーション手順、キーが失われた場合に何が壊れるか — については、シークレットとキー管理を参照してください。

プレビューデプロイメント

プレビューブランチをデプロイします:

wrangler deploy --env preview

wrangler.jsoncに環境セクションを追加します:

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "emdash-db-preview",
				},
			],
		},
	},
}

トラブルシューティング

「D1 binding not found」

wrangler.jsoncのバインディング名がデータベース設定と一致していることを確認します:

// 一致する必要があります: d1({ binding: "DB" })
"binding": "DB"

「R2 binding not found」

R2バケットが正しくバインドされていることを確認します:

// 一致する必要があります: r2({ binding: "MEDIA" })
"binding": "MEDIA"

マイグレーションエラー

スキーマエラーが表示される場合、Workerログを追跡し(wrangler tail)、エラーを再現して基礎となるメッセージをキャプチャしてから、その出力でissueを提出してください。