部署到 Cloudflare

本页内容

本指南将 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": ["* * * * *"] },
}

DBMEDIALOADER 名称必须与 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 插件,请省略 sandboxRunnerLOADER 绑定。

添加 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.jsonpackage.json#emdash.seed 中的路径或 seed/seed.json 读取 — 以先找到的为准 — 并内联到包中。如果都不存在,则使用内置的默认种子。对现有数据库的后续部署会保持其内容不变。

要更改已部署站点的架构或内容模型,请参阅演进已部署的站点

将 Worker 放置在 D1 附近

Cloudflare 默认在访问者附近运行 Worker。EmDash 的服务器渲染请求会进行多次 D1 往返,因此请使用 Targeted Placement 在 D1 主节点附近运行 Worker,使这些请求更快。

Wrangler 接受 placement.mode: "targeted" 与恰好一个选择器:regionhosthostname。选择针对 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 的情况下被服务。

启用

  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 },
       // 其他公共路由可以使用不同的缓存生命周期。
     },
    });

    @astrojs/cloudflare 适配器检测到 cacheCloudflare() 并在生成的部署配置中启用 Workers Cache。

  2. 使用平台 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() }
存储Platform 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 下面的一个独立层。

自定义域名

首次部署会收到一个 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/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 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 地址接收回复,不改变已接受的发送者地址。

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。