部署到 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 設定時會新增更多繫結。其中之一是媒體轉換使用的 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(), // 必需——管理介面是一個 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 讀取——以先找到的為準——並內嵌到 bundle 中。如果都不存在,則使用內建的預設種子。針對現有資料庫的後續部署不會改變其內容。

要變更已部署網站的架構或內容模型,請參見演進已部署的網站

排程工作

Cloudflare 從一個 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 控制自己的快取。

啟用前需要了解的兩件事:

  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

新網站請使用建議路徑。僅在已依賴其 Cache API 行為時保留 cloudflareCache()

另外不要將它們與物件快取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/… 上的媒體,該端點直接從 R2 繫結讀取來源位元組,無需 HTTP 請求。這些轉換在 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 轉換。每個來源圖片和參數的唯一組合每日曆月計費一次,該月內的重複請求免費。Images 免費方案涵蓋每月 5,000 次唯一轉換。超過該限制後,快取的轉換仍會提供,但新的轉換會傳回 9422 錯誤,圖片請求失敗。

Cloudflare Access 驗證

如果你的組織使用 Cloudflare Access,可以用它作為驗證提供者代替 passkey,透過現有的身分提供者提供單一登入。以下設定啟用它:

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. 將 AI Search 命名空間繫結新增到 Worker 設定:

    {
    	"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() 外掛透過原生 send_email Worker 繫結使用 Cloudflare Email Sending 傳送真實電子郵件,無需外部 API 密鑰。

1. 接入傳送網域

在 Cloudflare 儀表板中,前往 Email 並驗證你傳送電子郵件的網域(或地址)。Email Sending 拒絕來自未驗證傳送者的訊息。

2. 新增繫結

wrangler.jsonc 中宣告 send_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 密鑰等)。金鑰在啟動時驗證;外掛密鑰加密在啟用後使用它。在每次部署中設定它,這樣密鑰無需後續設定變更即可得到保護。

金鑰由你提供,永遠不會儲存在資料庫中;只有加密的密文會被儲存。遺失它意味著遺失用它加密的每個密鑰。

使用以下命令生成金鑰並將其儲存為 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。