项目五 内容管理 CMS
用 D1 管内容状态、R2 管媒体、Worker 管发布边界,完成草稿到公开页面的闭环。
编辑与核验:橙宝书编辑团队 ·
完成标准
编辑者可以保存草稿、预览和发布;公开访客只能读取已发布版本;媒体对象与内容记录有稳定关联;缓存失效与代码回滚不会意外公开草稿或删除媒体。
前置条件与运行随书示例
示例位于 examples/content-cms,需要 Node.js 22、pnpm 和已安装依赖。本地 D1 与 R2 由 Wrangler 模拟:
cp examples/content-cms/.dev.vars.example examples/content-cms/.dev.vars
pnpm wrangler d1 migrations apply orange-book-cms --local --config examples/content-cms/wrangler.jsonc
pnpm wrangler dev --config examples/content-cms/wrangler.jsonc运行 node --test examples/content-cms/test/index.test.mjs,应看到 6 个测试通过。按 README 创建草稿应返回 201 和 version: 1;使用相同版本发布后返回 version: 2。公开读取 SQL 已固定 status = 'published',测试也会验证 R2 写入成功但 D1 失败时删除孤儿对象。
最小实现边界
示例提供 JSON 内容 API 与媒体对象闭环,不伪装成完整编辑器。公开 CMS 仍需真实用户会话、角色、审计历史、预览签名和编辑 UI。
内容与媒体分开
D1 保存文章元数据、正文、状态、作者、版本与发布时间;R2 保存原始图片、PDF 附件和导出文件;Worker 负责认证编辑接口、校验状态转换并渲染公开读取。静态应用资源使用 Workers Static Assets,避免把站点 JS/CSS 存进业务数据库。
| 数据 | 建议位置 | 原因 |
|---|---|---|
| slug、标题、正文、状态 | D1 | 需要查询、唯一约束与发布状态 |
| 图片、附件、导出文件 | R2 | 对象体积大、按 key 读取 |
| 前端 JS/CSS | Static Assets | 随应用版本发布 |
| 短期公开响应 | Cache API/缓存规则 | 降低重复渲染,但不是权威数据源 |
最小 Schema 与读取边界
文章至少包含 id、slug、status、title、body、version、published_at 与更新时间。公开读取必须在 SQL 层限制 status = 'published',不能先读取草稿再靠前端隐藏。
const article = await env.DB.prepare(
`SELECT slug, title, body, version, published_at
FROM articles
WHERE slug = ?1 AND status = 'published'`
).bind(slug).first();
if (!article) return Response.json({ error: 'not_found' }, { status: 404 });管理读取走独立认证路由,并检查作者/编辑角色。公开错误不回显内部 ID、草稿内容或 SQL。
草稿到发布
保存草稿
服务端限制 slug、标题和正文长度,使用 prepared statement。版本号或更新时间参与并发控制,避免两个编辑者无提示覆盖。
上传媒体
先校验类型、大小和对象 key,再写 R2。数据库只保存受控 key、内容类型、大小、alt 文本与所有者。公开 URL 由服务端映射,不接受任意外部 URL 成为私有读取代理。
预览
预览路由需要认证或短期签名,响应明确禁止公开缓存。预览页面显示草稿 version,让编辑者知道自己看到哪次保存。
发布
发布操作验证状态转换和角色,写入 published_at 并增加 version。公开读取只返回已发布版本,再按 URL 和 version 设计缓存键。
验证失效
发布后请求旧缓存键和新版本 URL,证明不会继续展示旧正文。Cache API 的操作具有数据中心局部语义,不能假设一次本地 delete 等于全球业务状态回滚;权威发布状态始终在 D1。
阅读体验与 SEO
内容摘要
结构数据
媒体
双语
安全与失败路径
编辑、发布、删除和上传分别授权;登录/发布接口有 Turnstile 或速率策略与应用审计。测试空 slug、重复 slug、超长正文、非编辑者、草稿公开读取、R2 写失败和 D1 写失败。先写 R2 后写 D1 时,数据库失败会留下孤儿对象,需要可重试清理;先写 D1 后写 R2 时,公开读取必须处理媒体未就绪状态。
验证与排障
| 现象 | 先检查 | 恢复动作 |
|---|---|---|
管理接口返回 401 | EDITOR_KEY 是否只存在服务端且请求头一致 | 重建本地 .dev.vars,不要把 Key 放进前端 |
更新/发布返回 409 | 客户端的 expectedVersion 是否仍是当前草稿版本 | 重新读取草稿并让编辑者处理冲突 |
上传返回 415 | MIME 与 JPEG/PNG/WebP/PDF 文件签名是否一致 | 拒绝伪装文件,重新导出受支持格式 |
媒体返回 media_unavailable | D1 记录指向的 R2 object 是否存在 | 暂停公开引用并从版本化源恢复对象 |
远程边界与回滚
远程 D1 migration、R2 bucket、域名和缓存规则都需要单独确认。代码回滚不能让旧代码误读新内容 Schema。发布回滚优先创建一个新的已发布 version 指向上个稳定内容,而不是删除历史。媒体删除采用延迟清理和引用检查;发现误发布时先取消公开状态与清理缓存,再保留审计记录。
下一步:为 CMS 选择搜索方案。
官方来源
这篇内容帮你完成目标了吗?
内测反馈只在当前浏览器生成,不会自动上传。