本指南將 EmDash 網站部署到 Cloudflare Workers,使用 D1 作為資料庫、R2 作為媒體儲存。可從 EmDash Cloudflare 範本開始,或將相同組態套用到現有 Astro 網站。
先決條件
- Cloudflare 帳戶
- 已安裝專案相依套件
- Wrangler 已通過 Cloudflare 認證(
pnpm wrangler login)
設定繫結
Cloudflare 範本包含完整的 Worker 進入點以及具名的 D1 與 R2 繫結。首次部署時,若設定的名稱尚不存在,Wrangler 會建立相應資源。請保留 wrangler.jsonc 中的名稱;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 執行排程發佈、外掛工作、備份與維護。若網站使用沙箱外掛,參見 Plugin sandbox。
設定 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(),
}),
],
});
若網站不使用 marketplace、registry 或 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 匯出會無害。若同一專案稍後可能啟用外掛,請保留它。
若要在非每分鐘的排程上執行一般維護,將同一 Cron 運算式傳給 createScheduledHandler({ generalCron: "..." }) 和 triggers.crons。若二者不同,處理函式會記錄並忽略意外觸發。
建置與部署
建置並部署網站一次,讓 Wrangler 佈建具名的 D1 資料庫與 R2 儲存桶。Wrangler 使用 pnpm wrangler login 建立的本機登入。
pnpm build
pnpm wrangler deploy
在預設的 auto 遷移模式下,已部署的 Worker 收到第一個請求時,EmDash 會套用待處理的核心遷移。當部署管線必須在新程式碼接收流量之前套用遷移,或需要檢查、核對或復原遷移時,請使用 Manage core database migrations。
若資料庫為空(無 collection)且尚未完成設定精靈,EmDash 還會在首次開機時套用 seed 檔案。Seed 在建置時從 .emdash/seed.json、package.json#emdash.seed 中的路徑或 seed/seed.json(以先找到的為準)讀取,並內嵌到套件中。若都不存在,則使用內建預設 seed。對既有資料庫的後續部署不會改動其內容。
要變更已部署網站的 schema 或內容模型,參見 Evolving a Deployed Site。
將 Worker 放在靠近 D1 的位置
Cloudflare 預設在靠近訪客處執行 Worker。EmDash 伺服器端轉譯請求會多次往返 D1,因此請使用 Targeted Placement 在靠近 D1 主庫處執行 Worker,以加快這些請求。
Wrangler 接受恰好帶有一個選擇器的 placement.mode: "targeted":region、host 或 hostname。選擇針對 D1 主庫位置的值,並將得到的 placement 物件新增到 wrangler.jsonc。不要與 Targeted Placement 一起啟用 D1 唯讀複本。將 EmDash 的 session 設定保持預設 "disabled",使讀寫使用附近的主庫。
Object cache
為降低 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 設定、選項與失效行為見 Object Cache。
Workers Cache
Cloudflare 的 Workers Cache 在 Worker 前面放置邊緣快取:相符的請求會在完全不執行 Worker 的情況下被提供。
啟用
-
使用 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 }, // Other public routes can use different cache lifetimes. }, });@astrojs/cloudflare配接器會偵測cacheCloudflare(),並在產生的部署組態中啟用 Workers Cache。 -
使用平台 API 從 Worker 程式碼清除快取回應。此呼叫不需要 Cloudflare REST 憑證。
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 控制自身快取。
啟用前須知兩點:
- 沒有
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() | |
|---|---|---|
| Config | "cache": { "enabled": true } + 來自 @astrojs/cloudflare/cache 的 cacheCloudflare() | 來自 @emdash-cms/cloudflare 的 cache: { provider: cloudflareCache() } |
| Storage | Platform Workers Caching | Cache API(caches.open / put / match) |
| Purge | 來自 cloudflare:workers 的 cache.purge() | Zone REST POST /zones/{id}/purge_cache |
| Secrets | 清除無需 | CF_ZONE_ID + CF_CACHE_PURGE_TOKEN |
新網站請使用首選路徑。僅當已依賴其 Cache API 行為時才保留 cloudflareCache()。
也不要將上述任一與 object cache(objectCache: kvCache({ binding: "CACHE" }))混淆,後者在 KV 中快取資料庫查詢結果——是 Worker 下的單獨一層。
自訂網域
首次部署會獲得 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/ 前綴,因此不要透過公開網域暴露該前綴。Choose media storage 說明了安全邊界。
影像轉換
EmDash 透過 Cloudflare 的 IMAGES 繫結,在 Worker 內調整大小並重新編碼 R2 媒體。emdash/ui 的 Image 元件與豐富文字中的影像都透過 EmDash 在 Cloudflare 配接器下安裝的影像端點轉譯。對於內部路由 /_emdash/api/media/file/… 上的媒體,該端點直接從 R2 繫結讀取來源位元組,無需 HTTP 擷取。這些轉換在 Cloudflare Access 之後以及使用 global_fetch_strictly_public 時仍可運作。從儲存桶 URL 提供的媒體——見 Public R2 Access——則使用配接器自己的轉換端點,該端點在轉換前透過 HTTP 擷取檔案。
你不必宣告該繫結。@astrojs/cloudflare 會在 astro 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 計費。來源影像與參數的每個唯一組合每個日曆月計費一次,該月內的重複請求免費。若網站有 500 張來源影像,且每張請求一個縮圖尺寸和一個主圖尺寸,則這兩套參數在該月計為 1,000 次轉換影像。Images Free 方案每月涵蓋 5,000 次唯一轉換。超出該限制後,已快取的轉換仍會提供,但新轉換會回傳 9422 錯誤,影像請求失敗。
Cloudflare Access 認證
Cloudflare Access 可用附加到 Access 應用程式的身分提供者取代通行金鑰認證。受眾值是機密執行時期設定;透過命名其環境變數將其排除在 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,
},
}),
}),
使用 pnpm wrangler secret put CF_ACCESS_AUDIENCE 設定 CF_ACCESS_AUDIENCE。authentication guide 說明使用者佈建、預設角色與角色同步。
電子郵件
正式 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 下啟用外掛,並在 Settings → Email 下選擇它。在寄件者被接受且繫結存在之前,傳送會失敗。
除非其 binding 選項命名了另一個,否則外掛使用名為 EMAIL 的繫結。若它是唯一活動的電子郵件提供者,EmDash 會自動選擇它。若有多個提供者活動,請在 Settings → Email 下選擇 Cloudflare 提供者。可選的 replyTo 位址在不變更已接受 From 位址的情況下接收回覆。外掛可為單封訊息設定 replyTo,從而覆寫該訊息的此選項。
Cloudflare AI Search
AI Search 外掛需要原生外掛註冊與 ai_search_namespaces 繫結。部署後,在管理中開啟 Cloudflare AI Search,選擇 collection,並執行 Sync All Content。初始同步會索引啟用外掛之前發佈的內容;hooks 會保持後續變更同步。
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";
將搜尋介面新增到版面。觸發插槽接受與網站設計相符的按鈕:
---
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 加密宣告為密鑰的外掛設定。在儲存外掛密鑰之前設定它,並與 D1 備份分開保存。輪替期間,先提供新密鑰,並在逗號後保留較舊密鑰,直到每個外掛密鑰再次儲存。Secrets and key management 描述輪替與復原。
外掛橋直接讀取此 Worker 密鑰繫結。產生的管理設定路由透過 process.env 讀取它。使用 nodejs_compat 時,Cloudflare 對相容日期 2025-04-01 及之後預設填入 process.env。固定到更早日期的專案在儲存加密設定之前還必須新增 nodejs_compat_populate_process_env。
EmDash 在執行時期從 process.env 讀取密鑰。Worker 程式碼從 cloudflare:workers 匯入的 env 讀取繫結。切勿透過 import.meta.env 讀取密鑰:Vite 在建置時取代這些值,並可能將它們寫入伺服器套件。
預覽 HMAC 密鑰與評論者 IP 鹽除非你提供執行時期覆寫,否則會產生並儲存在資料庫中。Secrets and key management 列出確切變數、儲存位置與輪替效果。
預覽部署
具名的 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、管理登入、媒體上傳以及任何可選繫結。切勿將預覽繫結指向正式資料庫或儲存桶。
驗證部署
部署後,請求一個公開頁面,登入 /_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"
遷移錯誤
若看到 schema 錯誤,追蹤 Worker 記錄(wrangler tail)並重現錯誤以擷取底層訊息——然後帶著該輸出提交 issue。