先把最容易误解的事实说清楚:正式文章从未下线。
变更只存在于上线前隔离分支。隔离分支与人工审阅使该提交没有进入 main;现有发布门禁还要求请求指向当前 main,因此该测试分支不能直接推进 production。
这次往返测试由 Codex 在站主设定的发布边界内执行,后续修复依据可复查的提交和构建结果确定。测试过程中,一篇已经公开的旧文章在没有修改正文的情况下被保存,随后产生的提交给 frontmatter 新增了:
draft: true
如果这次提交被当成“无内容变化”继续进入生产构建,这篇文章会从路由、首页、知识列表、RSS、sitemap 和 JSON-LD 中一起消失。隔离测试发现变更,人工审阅阻止它继续进入发布流程。失败保存提交随后被恢复,没有进入任何正式发布历史。
这次测试改变了验收标准:CMS 显示保存成功、Markdown AST 与可见文字指纹不变,都不能单独证明往返安全。必须比较 frontmatter 的语义,并检查最终公开输出。

为什么一次“什么都没改”的保存仍会改变文章
接入 Pages CMS 时,新内容的 draft 字段被配置为 Boolean,默认值是 true。这个默认值本身没有错:新文章应该先是草稿。
问题出在旧内容。接入当时的四篇已发布 Markdown 文件没有显式 draft 字段,旧版代码把“字段缺失”视为已发布。本次测试中,保存旧文件后,Pages CMS 将配置中的默认值写入了此前缺失的字段。于是一次没有正文修改的保存改变了文章的发布状态。
失败保存提交的 diff 还包含标题引号、正文前空行和 EOF 换行的格式归一化。这些差异很显眼,但真正会改变公开结果的是一行 draft: true。
正文语义没变,为什么测试仍然失败
如果只比较 Markdown 正文,测试可能会得到“内容一致”的结论。但对静态站来说,frontmatter 也是程序输入。
当前实现中:
getPublishedKnowledge()只返回entry.data.draft === false;- 详情页静态路由使用同一发布规则;
- 首页、知识列表和 RSS 复用已发布集合;
- sitemap 在构建配置中排除草稿 URL;
- 草稿详情页不输出公开文章的 JSON-LD;
- 构建后验证会拒绝任何草稿路由或 slug。
所以正文没有变化,只能证明“正文没有变化”。它不能证明路由、元数据和分发入口没有变化。
这和把项目材料整理成公开知识时的判断类似:公开内容不只是文字,还包括状态、日期、类型和它进入哪些公开入口。
先撤销失败保存,再显式迁移发布状态
发现问题后,处理过程没有直接在失败保存提交上继续修补。恢复提交撤销了 Pages CMS 保存产生的变化,让隔离分支回到保存前的语义状态。
这一步的目的不是让 diff 看起来整洁,而是保住证据边界:
- 失败保存提交记录真实风险;
- 恢复提交记录回到安全状态的过程;
- 两个提交均未用于推进发布分支,也未合入正式候选;
- 后续发布从干净 release candidate 重新组织。
如果把失败提交和恢复提交一起合入,最终文件也许相同,但正式历史会混入一次已经知道不安全的发布状态变化。保留测试分支、用干净候选发布,更容易解释每个提交的目的。
真正的修复没有把 Pages CMS 的新内容默认值改成 false。那会让未来新建文章直接变成“已发布”,只是把风险从旧内容转移到新内容。
最终实现保留了新内容默认草稿的规则,并让迁移过程给接入当时的四篇公开内容显式增加 YAML Boolean:
draft: false
同时把约束收紧到三个位置:
- Astro 内容 Schema 要求
draft是 Boolean; - 仓库验证脚本拒绝字段缺失和字符串
"false"; - production 查询只公开严格等于
false的条目。
这样,旧内容和新内容都不再依赖“字段缺失时猜一个默认状态”。发布意图必须写进文件。
merge: true 为什么没有自动解决问题
.pages.yml 中保留了:
settings:
content:
merge: true
它很重要,因为 Pages CMS 保存结构化内容时,会保留编辑器 Schema 没有声明的 frontmatter 字段。但它解决的是“未知字段会不会丢失”,不是“已配置字段应该取什么值”。
draft 已经是编辑器明确管理的字段,并且配置了默认值。merge: true 不会拒绝这个字段被写回。数据安全不能只靠 merge,还需要显式迁移、类型约束和真实往返测试。
第二轮往返测试检查了什么
修复后,第二轮非生产验证覆盖了不同语言 collection,而不是只重跑原来的中文文件:
- 中文旧文章保存后,
draft保持 Booleanfalse; - 英文旧文章保存后,
draft同样保持 Booleanfalse; - frontmatter 允许 Pages CMS 做格式归一化,但字段语义、Markdown AST、渲染 HTML 和可见文字保持一致;
- 临时中文和英文内容默认
draft: true,并使用同一个translationKey; - 测试图片、两篇临时草稿和对应 GitHub Actions Preview 构建检查完成后,由测试内容清理提交移除;
- production build 中没有临时路由、图片引用或草稿 slug。
这里的“幂等”不是要求 Pages CMS 永远保持每个空行和引号样式,而是第二次保存不能继续改变内容语义或公开状态。
一份可复用的 CMS 往返测试清单
以后再为文件型站点接入 CMS,至少应检查以下项目:
- 保存前建立语义指纹:记录 frontmatter 类型、正文 AST、渲染 HTML 和可见文字,不只记录正文哈希。
- 每种 collection 测一个旧文件:不同语言或 Schema 不能只抽查一份。
- 执行一次无内容修改保存:检查默认值、日期、数组、引号和未知字段。
- 再保存一次归一化结果:确认第二次语义幂等。
- 新建真实草稿:验证默认状态、双语配对、媒体路径和 Preview。
- 检查公开产物:确认草稿不进入任何公开入口。
- 清除测试内容:最终候选 diff 不能残留临时文章和图片。
- 隔离失败证据:失败提交保留在验证分支,正式候选从干净状态重建。
其中最重要的一条是:不要把编辑器的成功提示当作发布安全证明。提示只能证明一次保存请求完成了,不能证明文件语义和网站输出没有改变。
这个风险的适用边界
这不是“Pages CMS 会把所有旧文章变成草稿”的普遍结论。触发条件很具体:旧文件缺少字段,编辑器 Schema 为这个字段提供了默认值,保存又把默认值写回仓库。
同类风险也可能出现在其他 Git-backed CMS、表单编辑器或内容迁移脚本中。字段名不一定叫 draft,也可能是 published、featured、日期、语言或权限状态。可复用的结论不是避开某个工具,而是把“无修改保存”当成一次数据迁移来验证。
这也是保留新内容 default: true 的原因:正确目标不是让所有内容默认公开,而是让每个文件明确表达自己的状态,让生产构建只接受经过验证的公开值。