本指南将 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。
在配置向导完成之前,EmDash 还会在第一个请求时应用一次 seed 文件的 schema 与结构。仅当管理员在配置向导中选择时,才会应用 seed 中的示例内容。Seed 在构建时从 .emdash/seed.json、package.json#emdash.seed 中的路径或 seed/seed.json(以先找到的为准)读取,并内联到包中。若都不存在,则使用内置默认 seed。对已有数据库的后续部署不会改动其内容。
要更改已部署站点的 schema 或内容模型,参见 Evolving a Deployed Site。
排期发布
在 Cloudflare 上,EmDash 只会通过 Worker 的 scheduled 处理函数发布已排期的条目,Cloudflare 会按照 Cron Trigger 的计划调用该处理函数。已部署的 Worker 需要同时具备以下两项:
wrangler.jsonc的triggers.crons列表中有一个 Cron Trigger,如配置绑定所示。- 来自
@emdash-cms/cloudflare/worker的scheduled处理函数,从main指向的 Worker 入口点导出,如添加 Worker 入口点所示。
若缺少其中任何一项,已排期的条目都不会上线。在使用 astro dev 的本地开发中,EmDash 会用定时器发布已排期的条目,因此在本地可行的排期并不能证明已部署的配置正确。
在项目根目录运行 emdash doctor,检查本地项目文件中的 Cron Trigger 与 scheduled 处理函数:
pnpm emdash doctor
当 emdash doctor 同时找到两者时,输出会包含一项通过的 scheduler wiring 检查。失败的调度器检查会描述问题并打印修复方法。该检查能识别本页展示的 Worker 入口点,以及它作为修复方法打印的单行重新导出,因此以其他方式编写的入口点即使能正常工作,也可能无法通过该检查。
若没有出现调度器检查,说明 emdash doctor 在当前目录中没有找到 Wrangler 配置,或者既没有找到 Cron Trigger 也没有找到 scheduled 处理函数。在 D1 项目上,database 检查会失败,因为它查找的是本地 SQLite 文件(默认为 ./data.db)。该失败不影响调度器检查。
添加或更改 Cron Trigger 或 scheduled 处理函数后请重新部署。Cloudflare 可能需要最多 15 分钟才会开始运行新的或已更改的 Cron Trigger。触发器开始运行后,EmDash 会发布排期时间已过的条目。
要观察触发器,请运行 pnpm wrangler tail。每次运行都会打印类似 "* * * * *" @ 9/29/2026, 10:01:00 AM - Ok 的一行。Ok 表示该次运行结束时没有未捕获的错误。EmDash 会捕获并记录发布错误,因此 Ok 并不能确认条目已发布。发布了条目的运行还会记录类似 [scheduled] Published 1 scheduled item(s) 的消息,该次运行的错误会出现在同一输出中。
部署后,当某个已排期的条目已超过其排期时间,且最近五分钟内没有完成任何调度器运行时,管理仪表盘会显示 Scheduled publishing needs attention。如果 Cron Trigger 的运行频率低于每五分钟一次,该提示也可能在两次运行之间出现,直到下一次运行发布该条目。
将 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 设置、选项与失效行为见 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),因此不会向另一方向泄漏。
Workers Caching 与对象缓存(objectCache: kvCache({ binding: "CACHE" }))是分开的,后者在 Worker 之下的 KV 中缓存数据库查询结果。
自定义域名
首次部署会获得 workers.dev URL。自定义域名必须已是与 Worker 同一账户中由 Cloudflare 管理的活动域名。Worker 在其 workers.dev URL 成功响应后,将生产域名添加为 Wrangler 路由:
{
"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}
再次部署并验证两个地址。在测试 DNS 时保持 workers.dev 地址可用,有助于区分路由问题与应用问题。若要让新域名成为站点地图和搜索元数据中的地址,以及在未设置 siteUrl、EMDASH_SITE_URL 或 SITE_URL 时邮件和插件中使用的地址,请使用 Settings > General 中的 Change domain;参见迁移到新域名。
如果没有设置 siteUrl、EMDASH_SITE_URL 或 SITE_URL,设置会将其运行所在的地址记录为站点的源,设置期间创建的通行密钥只能在该地址上使用。请在生产域名上运行设置,或在设置之前将 EMDASH_SITE_URL 设为该域名。设置之后若将 EMDASH_SITE_URL 改为另一个域名,登录会转移到该域名,而在旧地址上创建的通行密钥将无法在新域名上使用。
公开 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 提供的媒体——见公开 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。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: "[email protected]", name: "My Site CMS" },
replyTo: "[email protected]",
}),
],
}),
该插件自首次部署起即处于激活状态。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 列出确切变量、存储位置与轮换效果。
预览部署
预览既可以作为 Worker Preview 运行(它会在生产 Worker 上为每个分支提供独立的 URL),也可以作为命名 Wrangler 环境中的独立 Worker 运行。无论哪种方式,预览都需要与生产分开的数据库和存储桶。切勿将预览绑定指向生产数据库或存储桶。
Worker Previews
Worker Previews 要求项目依赖中的 Wrangler 为 4.135.0 或更高版本。由较旧的 EmDash 模板创建的项目可能锁定了更早的版本;请使用 pnpm add -D wrangler@latest 升级。预览不会继承顶层配置中的绑定或 vars;请在 previews 块中列出预览使用的每个绑定。
Astro Cloudflare 适配器会添加一个 SESSION KV 绑定。wrangler deploy 会自动预配它,但 wrangler preview 在没有命名空间 ID 时会拒绝它。请创建预览资源,包括用于会话的 KV 命名空间:
pnpm wrangler d1 create my-emdash-site-preview
pnpm wrangler r2 bucket create my-emdash-media-preview
pnpm wrangler kv namespace create my-emdash-site-preview-session
将 Wrangler 输出的资源标识符添加到 wrangler.jsonc:
{
"previews": {
"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",
},
],
"kv_namespaces": [{ "binding": "SESSION", "id": "00000000000000000000000000000000" }],
"worker_loaders": [{ "binding": "LOADER" }],
},
}
当预览使用 send_email、AI Search、vars 及其他功能时,请重复声明这些绑定。
设置向导和通行密钥需要预览的站点 URL。如果没有 siteUrl 或 EMDASH_SITE_URL,设置会失败并报 SITE_URL_REQUIRED。每次部署预览时,都要传入预览 URL https://<preview-name>-<worker-name>.<subdomain>.workers.dev:
pnpm build
pnpm wrangler preview --name my-branch \
--var EMDASH_SITE_URL:https://my-branch-my-emdash-site.example.workers.dev
请在 --name 和 URL 中使用相同的预览名称。astro.config.mjs 中的 siteUrl 会覆盖 EMDASH_SITE_URL,并被构建进每个预览,因此预览上的通行密钥会绑定到生产域名,设置和登录在预览上都无法使用。请改为在顶层 vars 中用 EMDASH_SITE_URL 设置生产 URL。
如果 Worker 将 workers_dev 设为 false,则在你于 wrangler.jsonc 顶层设置 "preview_urls": true 并运行一次 wrangler deploy 之前,预览没有 URL。wrangler preview 不会应用这项设置。wrangler deploy 会把当前检出中的代码(而不只是这项设置)发布到生产环境,因此请提交这一更改,并在生产分支的干净检出中部署。然后回到你的分支,再次运行 wrangler preview。
用 pnpm wrangler preview secret put <NAME> --name my-branch 为某一个预览设置密钥。pnpm wrangler preview base-config secret put <NAME> 会为此后创建的预览设置密钥;已有的预览保留它们原来的密钥。
请留意以下与生产环境的差异:
- 预览共享
previews块中的资源。wrangler preview读取其所部署分支的previews块,因此所有沿用基础分支配置的分支都使用同一个数据库。对预览的第一次请求会应用待处理的核心迁移,所以升级 EmDash 的分支也会迁移其他预览所使用的数据库。若要隔离某个分支,请再创建一个 D1 数据库,并在部署前修改该分支previews块中的database_name和database_id。 - 空的预览数据库会显示设置向导。 预览 URL 是公开的,第一个完成向导的访客会成为管理员。请在创建预览后立即完成设置,或用 Cloudflare Access 保护预览 URL。
- Cron Triggers 不会运行。 定时文章不会发布,插件的 cron 任务和定时备份也不会在预览上运行。
- 通行密钥只在注册它的主机名上有效。 生产环境的通行密钥在预览上无效,在第一个预览上创建的管理员只能在该预览 URL 上用通行密钥登录。在共享该数据库的另一个预览上,请使用魔法链接登录(它需要邮件提供方及其在
previews块中的绑定),并在那里添加通行密钥。或者,如上所述为该分支单独使用一个数据库来隔离它,并在其上运行设置。
命名环境
命名的 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,上传并检索测试媒体文件,并确认 scheduled 处理函数出现在 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。