本指南将 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 会应用待处理的核心迁移。Worker 会把大型内容迁移留给 emdash migrate,并在这些迁移应用之前以 503 响应应答。当部署流水线必须在新代码接收流量之前应用迁移,或需要检查、核对或恢复迁移时,请使用 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 只读副本。没有副本时,每个查询都使用附近的主库,包括使用 session: "auto" 和 coalesce: true 时。
对象缓存
为降低 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 设置缓存标头,并让提供商能在内容变更时使条目失效。
import { cacheCloudflare } from "@astrojs/cloudflare/cache";
export default defineConfig({
adapter: cloudflare(),
cache: {
provider: cacheCloudflare(),
},
routeRules: {
"/": { maxAge: 300, swr: 86400, tags: ["posts", "pages"] },
// 其他公开路由可以使用不同的缓存时长。
},
});
@astrojs/cloudflare 适配器会检测 cacheCloudflare(),并在生成的部署配置中启用 Workers Cache。
内容变更时,EmDash 会自动使缓存条目失效(标签约定参见路由缓存)。只有对于自定义标签,例如手动的“清除全部”开关,你才需要自行清除,方法是在 Worker 中使用平台的清除 API。它不需要 Cloudflare REST 凭证。
公开页面通过 routeRules 和 Astro.cache 控制自身的缓存。请仅为你的公开路由设置路由规则:站点根路径上的剩余参数,例如 /[...slug],同样会匹配 /_emdash/ 下的管理后台和 API 路由。
启用前须知两点:
- 带有任何缓存提示的响应都是可缓存的,即使没有路由规则。 适配器会把没有任何路由规则或
Astro.cache.set()调用将其设为可缓存的响应标记为no-store。带有标签但没有maxAge的提示,例如布局在每个页面上设置的站点设置和菜单提示,会产生不带有效期的Cloudflare-CDN-Cache-Control: public,因此 Workers Cache 会回退到它的默认缓存时长(200响应为 2 小时)。请为每个公开路由提供规则,并让依赖会话的路由不带任何缓存提示。 - 缓存页面会与已登录编辑者共享。 缓存在 Worker 之前运行,因此无法根据请求 cookie 旁路。使用默认的服务端渲染工具栏时,已登录编辑者可能收到公开页面的缓存匿名变体(没有可视化编辑工具栏),直到条目过期。请设置
toolbar: "client",以便在缓存页面上保持工具栏可靠。为已登录用户渲染的页面绝不会存入路由缓存。
如需更深入的指南,涵盖路由规则、缓存标签、EmDash 在发布时的内置失效机制,以及必须让其不进入缓存的响应,请参阅路由缓存。
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
要为站点添加 Cloudflare AI Search,请安装 AI Search 插件。
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。