本指南將 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 執行排程發佈、外掛任務、備份和維護。如果站點使用沙盒外掛,請參閱外掛沙盒。
設定 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(), // 必需 — 管理 UI 是一個 React 應用程式
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 匯出是無害的。如果同一專案以後可能啟用外掛,請保留它。
要以不同於每分鐘的排程執行一般維護,請將相同的 Cron 運算式傳遞給 createScheduledHandler({ generalCron: "..." }) 和 triggers.crons。如果它們不同,處理器會記錄並忽略意外的觸發器。
建置和部署
建置並部署站點一次,讓 Wrangler 佈建具名的 D1 資料庫和 R2 儲存桶。Wrangler 使用 pnpm wrangler login 建立的本機登入。
pnpm build
pnpm wrangler deploy
使用預設的 auto 遷移模式,EmDash 在部署的 Worker 收到第一個請求時套用待處理的核心遷移。當部署管線需要在新程式碼接收流量之前套用遷移,或需要檢查、驗證或復原遷移時,使用管理核心資料庫遷移。
如果資料庫為空(沒有集合)且設定精靈尚未完成,EmDash 還會在首次啟動時套用種子檔案。種子在建置時從 .emdash/seed.json、package.json#emdash.seed 中的路徑或 seed/seed.json 讀取 — 以先找到的為準 — 並內嵌到套件中。如果都不存在,則使用內建的預設種子。對現有資料庫的後續部署會保持其內容不變。
要變更已部署站點的架構或內容模型,請參閱演進已部署的站點。
將 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",以便讀寫使用附近的主節點。
物件快取
要減少 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()。import { cacheCloudflare } from "@astrojs/cloudflare/cache"; export default defineConfig({ adapter: cloudflare(), cache: { provider: cacheCloudflare(), }, routeRules: { "/": { maxAge: 300, swr: 86400 }, // 其他公開路由可以使用不同的快取生命週期。 }, });@astrojs/cloudflare配接器偵測到cacheCloudflare()並在產生的部署設定中啟用 Workers Cache。 -
使用平台 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 控制自己的快取。
啟用前需要知道的兩件事:
- 沒有
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() } |
| 儲存 | Platform Workers Caching | Cache API(caches.open / put / match) |
| 清除 | cloudflare:workers 的 cache.purge() | Zone REST POST /zones/{id}/purge_cache |
| 密鑰 | 清除不需要 | CF_ZONE_ID + CF_CACHE_PURGE_TOKEN |
新站點使用推薦路徑。僅當你已經依賴其 Cache API 行為時才保留 cloudflareCache()。
也不要將這兩者中的任何一個與物件快取(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/ 前綴,因此不要透過公開網域暴露該前綴。選擇媒體儲存解釋了安全邊界。
影像轉換
EmDash 透過 Cloudflare 的 IMAGES 繫結在 Worker 內部對 R2 媒體進行調整大小和重新編碼。emdash/ui 的 Image 元件和富文本中的影像都透過 EmDash 在 Cloudflare 配接器下安裝的影像端點進行渲染。對於內部路由 /_emdash/api/media/file/… 上的媒體,該端點直接從 R2 繫結讀取來源位元組,無需 HTTP 擷取。這些轉換在 Cloudflare Access 後面和使用 global_fetch_strictly_public 時仍然有效。從儲存桶 URL 提供的媒體 — 參見公開 R2 存取 — 使用配接器自己的轉換端點,該端點在轉換前透過 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。驗證指南解釋了使用者佈建、預設角色和角色同步。
電子郵件
正式環境 Worker 沒有預設的電子郵件傳送服務。魔法連結登入、團隊邀請和留言通知在電子郵件外掛啟用之前傳回電子郵件未設定。
Cloudflare 電子郵件外掛使用 send_email 繫結。首先使用 Cloudflare Email Sending 接入並驗證寄件者網域。Cloudflare 拒絕寄件者位址不是已接受寄件者的訊息。
新增繫結並註冊提供者:
{
"send_email": [{ "name": "EMAIL" }],
}
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";
emdash({
plugins: [
cloudflareEmail({
from: { email: "[email protected]", name: "My Site CMS" },
replyTo: "[email protected]",
}),
],
}),
部署後,在擴充功能中啟用外掛並在設定 → 電子郵件中選擇它。在寄件者被接受且繫結存在之前,傳送會失敗。
外掛使用名為 EMAIL 的繫結,除非其 binding 選項指定了另一個名稱。如果它是唯一活動的電子郵件提供者,EmDash 會自動選擇它。如果有多個提供者活動,請在設定 → 電子郵件中選擇 Cloudflare 提供者。可選的 replyTo 位址接收回覆,不變更已接受的寄件者位址。
Cloudflare AI Search
AI Search 外掛需要原生外掛註冊和 ai_search_namespaces 繫結。部署後,在管理面板中開啟 Cloudflare AI Search,選擇集合並執行同步所有內容。初始同步會索引外掛啟用前發佈的內容;鉤子保持後續變更同步。
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="搜尋內容">
<button slot="trigger" type="button">搜尋</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 鹽會產生並儲存在資料庫中,除非你提供執行時覆寫。密鑰和密鑰管理列出了確切的變數、儲存位置和輪換效果。
預覽部署
具名的 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 中的繫結名稱與你的資料庫設定匹配:
// 必須匹配:d1({ binding: "DB" })
"binding": "DB"
“R2 binding not found”
檢查 R2 儲存桶是否正確繫結:
// 必須匹配:r2({ binding: "MEDIA" })
"binding": "MEDIA"
遷移錯誤
如果看到架構錯誤,追蹤 Worker 日誌(wrangler tail)並重現錯誤以擷取底層訊息 — 然後用該輸出提交 Issue。