部署到 Cloudflare

本页内容

Cloudflare Workers 为 EmDash 提供了快速、全球分布式的运行时。本指南介绍使用 D1 作为数据库和 R2 作为媒体存储的部署方式。

前提条件

  • 一个 Cloudflare 账户
  • 已安装 Wrangler CLI(npm install -g wrangler
  • 已通过 Cloudflare 认证(wrangler login

配置绑定

配置生产 D1 数据库和 R2 存储桶,然后在项目根目录创建 wrangler.jsonc,为其不可变 ID 和名称设置绑定。数据库配置与应用 EmDash 的模式迁移是分开的。

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "my-emdash-site",
	"compatibility_date": "2025-01-15",
	"compatibility_flags": ["nodejs_compat"],

	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "emdash-db",
			"database_id": "00000000-0000-0000-0000-000000000000",
		},
	],

	"r2_buckets": [
		{
			"binding": "MEDIA",
			"bucket_name": "emdash-media",
		},
	],
}

这些是你自己配置的绑定。@astrojs/cloudflare 适配器在生成已部署的 Worker 配置时会添加更多绑定。其中之一是媒体转换使用的 IMAGES 绑定——参见图片转换

沙盒插件——市场安装和 sandboxed: [] 下的插件——需要 worker_loaders 绑定和一个导出 PluginBridge 的 Worker 入口点。参见插件沙盒

配置 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 } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // 必需——管理界面是一个 React 应用
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

迁移与部署

运行时迁移默认保持自动。对于部署管理的迁移,构建 Worker 并使用其账户和数据库 UUID 检查已配置的 D1 目标。

pnpm build
pnpm exec emdash migrate --status --json \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID"

在审查并记录报告的目标指纹后,应用迁移并部署相同的构建。

pnpm exec emdash migrate \
  --account-id "$CLOUDFLARE_ACCOUNT_ID" \
  --d1 "$D1_DATABASE_ID" \
  --expected-target-fingerprint "$EMDASH_TARGET_FINGERPRINT"
pnpm exec wrangler deploy

迁移作业需要具有 D1 Edit 权限的 CLOUDFLARE_API_TOKEN。按账户和数据库 UUID 序列化作业。有关配置、CI 并发、运行时模式和恢复指导,请参见管理核心数据库迁移

如果数据库为空(没有集合)且设置向导尚未完成,EmDash 还会在首次启动时应用种子文件。种子在构建时从 .emdash/seed.jsonpackage.json#emdash.seed 中的路径或 seed/seed.json 读取——以先找到的为准——并内联到 bundle 中。如果都不存在,则使用内置的默认种子。针对现有数据库的后续部署不会改变其内容。

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

计划任务

Cloudflare 从一个 Cron Trigger 运行计划发布、插件任务和常规维护。

使用标准的 Worker 入口点:

import handler, {
	createScheduledHandler,
	PluginBridge,
} from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

export default {
	...handler,
	scheduled: createScheduledHandler(),
} satisfies ExportedHandler;

wrangler.jsonc 中配置一个用于常规维护的 Cron Trigger:

{
	"triggers": {
		"crons": ["* * * * *"],
	},
}

要使用不同的常规维护计划,在 createScheduledHandler() 中设置 generalCron,并在 wrangler.jsonc 中使用相同的表达式。

部署

部署到 Cloudflare Workers:

wrangler deploy

你的站点现在在 https://my-emdash-site.<your-subdomain>.workers.dev 上线。

只读副本

对于全球分布式站点,启用 D1 只读复制,将读取查询路由到附近的副本,而不是始终访问主数据库。这显著降低了远离主区域的访问者的延迟。

emdash({
	database: d1({
		binding: "DB",
		session: "auto",
	}),
	storage: r2({ binding: "MEDIA" }),
}),

你还需要在 Cloudflare 仪表板或通过 REST API 在 D1 数据库本身上启用只读复制。

有关会话模式和基于书签的一致性工作原理,请参见数据库选项——只读副本

对象缓存

为了减少 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. wrangler.jsonc 中开启平台缓存:
{
	"cache": {
		"enabled": true,
	},
}
  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 },
		// …
	},
});

使用 cacheCloudflare() 时,@astrojs/cloudflare 适配器还会在生成的 Wrangler 配置中缺少时注入 "cache": { "enabled": true }——在你自己的 wrangler.jsonc 中明确列出使意图明显。

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

自定义域名

在 Cloudflare 仪表板中添加自定义域名:

  1. 转到 Workers & Pages > 你的 worker
  2. 点击 Custom Domains > Add Custom Domain
  3. 输入你的域名并按照 DNS 设置说明操作

公开 R2 访问

要直接从 R2 提供媒体(推荐以获得更好的性能):

  1. 在 Cloudflare 仪表板中,转到 R2 > 你的存储桶
  2. 点击 Settings > Public access
  3. 启用公共访问并记下公共 URL
  4. 更新你的存储配置:
storage: r2({
  binding: "MEDIA",
  publicUrl: "https://pub-xxx.r2.dev"
}),

图片转换

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 转换。每个源图片和参数的唯一组合每日历月计费一次,该月内的重复请求免费。Images 免费计划覆盖每月 5,000 次唯一转换。超过该限制后,缓存的转换仍会提供,但新的转换会返回 9422 错误,图片请求失败。

Cloudflare Access 认证

如果你的组织使用 Cloudflare Access,可以用它作为认证提供者代替 passkey,通过现有的身份提供者提供单点登录。以下配置启用它:

emdash({
  database: d1({ binding: "DB" }),
  storage: r2({ binding: "MEDIA" }),
  auth: access({
    teamDomain: "myteam.cloudflareaccess.com",
    audience: "your-app-audience-tag",
    roleMapping: {
      "Admins": 50,
      "Editors": 40,
    },
  }),
}),

完整的配置选项请参见认证指南

AI Search 插件索引已发布的 EmDash 内容,并为你的站点添加智能搜索界面。

  1. 在传递给 EmDash 的 plugins 数组中注册插件:

    import { aiSearch } from "@emdash-cms/cloudflare/plugins";
    
    // ...
    plugins: [
    	formsPlugin(),
    	aiSearch(),
    ],
  2. 将 AI Search 命名空间绑定添加到 Worker 配置:

    {
    	"ai_search_namespaces": [
    		{
    			"binding": "AI_SEARCH",
    			"namespace": "default",
    		},
    	],
    }
  3. 创建搜索界面使用的搜索端点:

    export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";
  4. 将搜索界面添加到站点布局。触发器插槽可以包含任何适合你站点设计的按钮:

    ---
    import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
    ---
    
    <AISearchSnippet apiUrl="/api/ai-search" placeholder="搜索...">
    	<button slot="trigger" type="button">搜索</button>
    </AISearchSnippet>
  5. 部署站点:

    pnpm exec wrangler deploy
  6. 在 EmDash 管理面板中打开 Cloudflare AI Search,选择要索引的集合,然后点击 Sync All Content

    此初始同步是必需的:插件的内容钩子仅在启用后创建或更新的内容上触发,因此在此之前发布的任何内容在你运行完全同步之前都不会出现在索引中。

设置后发布或更新的内容会自动保持同步。同一页面显示索引进度。

邮件

在 Workers 上,唯一内置的 email:deliver 处理程序是开发控制台存根,因此依赖邮件的流程——魔术链接登录、团队邀请和评论通知——在生产环境中会以 “Email is not configured” 失败。cloudflareEmail() 插件通过原生 send_email Worker 绑定使用 Cloudflare Email Sending 发送真实邮件,无需外部 API 密钥。

1. 接入发送域名

在 Cloudflare 仪表板中,转到 Email 并验证你发送邮件的域名(或地址)。Email Sending 拒绝来自未验证发送者的消息。

2. 添加绑定

wrangler.jsonc 中声明 send_email 绑定:

{
	"send_email": [{ "name": "EMAIL" }],
}

3. 注册提供者

将插件添加到你的 emdash() 集成中:

import { d1, r2 } from "@emdash-cms/cloudflare";
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	database: d1({ binding: "DB" }),
	storage: r2({ binding: "MEDIA" }),
	plugins: [
		cloudflareEmail({
			from: { email: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]", // 可选
			binding: "EMAIL", // 可选,默认为 "EMAIL"
		}),
	],
}),

4. 激活并选择

部署后,在 Admin → Extensions 下激活插件,并在 Settings → Email 下选择它作为提供者。

选项

选项类型默认值描述
fromstring | { email, name? }—(必需)已接入 Email Sending 的域名上的发送者地址。
replyTostring可选的 Reply-To,当 from 是 no-reply 子域名地址时很有用。
bindingstring"EMAIL"wrangler.jsoncsend_email 绑定的名称。

环境变量

推荐:加密密钥

EMDASH_ENCRYPTION_KEY 是用于加密静态插件密钥的密钥(webhook 令牌、Turnstile 密钥等)。密钥在启动时验证;插件密钥加密在启用后使用它。在每次部署中设置它,这样密钥无需后续配置更改即可得到保护。

密钥由你提供,永远不会存储在数据库中;只有加密的密文会被存储。丢失它意味着丢失用它加密的每个密钥。

使用以下命令生成密钥并将其存储为 Worker 密钥:

npx emdash secrets generate
wrangler secret put EMDASH_ENCRYPTION_KEY

可选:稳定值覆盖

EmDash 自动生成预览 HMAC 密钥和评论者 IP 哈希盐值,并在首次使用时将其持久化到数据库中。以下环境变量是需要自己固定值的情况的覆盖——例如,当单独进程中的预览 Worker 需要与主站点共享密钥时。

变量用途
EMDASH_PREVIEW_SECRET自动生成的预览 HMAC 密钥的覆盖。
EMDASH_IP_SALT自动生成的评论者 IP 哈希盐值的覆盖。
EMDASH_AUTH_SECRET可选。如果设置,将用作 IP 盐值来源(除非同时设置了 EMDASH_IP_SALT,后者优先),为已依赖它的安装保持评论者 IP 哈希稳定。新部署请保持未设置。

在配置中使用 import.meta.env 或 Cloudflare env 绑定访问环境变量。

有关 EmDash 使用的每个密钥的完整清单——包括存储位置、轮换步骤以及密钥丢失时会中断什么——请参见密钥与密钥管理

预览部署

部署预览分支:

wrangler deploy --env preview

wrangler.jsonc 添加环境部分:

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "emdash-db-preview",
				},
			],
		},
	},
}

故障排除

”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。