本篇是《手写一个内容型技术博客站》系列第 7 篇。
内容站的"部署"和纯应用不太一样:大多数故障不是代码坏,是内容坏——一个配置写错、一篇文章 frontmatter 有问题,可能不报错但页面悄悄消失。这篇讲如何在 build 之前拦住这些"静默故障"。
一、带病上线:三种"不报错却出错"的形态
这是推动我写 preflight 的真实案例:
| 形态 | 症状 | build 会不会报错 | 用户看到 |
|---|---|---|---|
| 目录未注册 | 磁盘有分类目录,config 没登记 | ❌ | 文章在列表/SEO 里静默消失 |
| frontmatter 字段错 | featured: "true" 字符串 | ❌ | 首页精选失效 |
| commentId 重复 | 两篇文章同一个 cid | ❌ | 评论串号(A 篇的评论出现在 B 篇) |
这三种共同的可怕之处:build 全绿、页面能开,但数据错了。最危险的是 commentId 重复——评论是真实用户数据,串号等于把 A 的评论暴露到 B 的文章下,不可逆。
二、preflight-check.mjs:5 步校验
于是有了 scripts/preflight-check.mjs——在 npm run build 之前跑,零外部依赖(只用项目已有的 gray-matter / js-yaml),退出码 0 通过 / 1 error / 2 warning。
[1/5] 检查 src/config/*.yaml
- YAML 能否解析
- 全角冒号(跳过 # 注释行,中文注释的全角冒号是正常写法)
[2/5] 检查 categories.yaml schema
- 分类 name/slug 重复
- order 缺失
[3/5] 检查 markdown frontmatter(每篇)
- title 类型/必填
- slug:安全字符(^[A-Za-z0-9][A-Za-z0-9-]*$ 含大写)+ 全局唯一性
- commentId:格式 + 全局唯一性
- featured:必须 boolean(字符串 "true" 报错)
- date/tags/excerpt/source 类型
- 字段名全角标点
[4/5] 目录 vs config 孤儿检查
- 磁盘有目录、config 没注册 → WARN
- config 注册了、目录名不匹配 → WARN
[5/5] 汇总(ERROR/WARNING 计数 + 退出码)三、几个关键校验的设计权衡
slug 白名单:大写是合法的
const SLUG_OK = /^[A-Za-z0-9][A-Za-z0-9-]*$/;最初写的是 ^[a-z0-9],结果存量 slug 大量报错——Hermes-Agent-Hindsight-configuration 这种含大写的 slug 一直在正常访问,强制小写会误报。校验必须兼容存量,白名单放宽到含大写。
缺 slug 只对"中文文件名 + 非教程"告警
判断"缺 slug 是否危险"要看谁会兜底:
- 英文文件名:
slug: data.slug || 文件名兜底,永远不会 404 → 不告警 - 中文文件名:无 slug 时回调中文文件名,Next 对中文动态段 SSG 匹配 → 404 → WARN
- 教程章节:按磁盘路径读取(
getTutorialBySlugServer),根本不需要 slug → 天然豁免
校验的上下文感知比"一刀切检查"重要得多——否则 20+ 篇教程章节全被误报。
commentId 唯一性:最危险的检查
评论串号 = 两篇文章共用一个 commentId,用户数据互相污染。这是整个 preflight 里最该 ERROR 的项:
// 跨文件唯一性
for (const [cid, files] of cidOccurrences) {
if (files.length > 1)
log.error("frontmatter", `commentId 重复(评论会串号): "${cid}" → ${files.join(" , ")}`);
}四、--fix:自动补默认字段 + 预览门闩
preflight 发现缺字段,能不能让它自动补?可以,但只补"有明确缺省值且零质量风险"的字段:
| 字段 | 缺省补 | 原因 |
|---|---|---|
| featured | false | 运行时 !!data.featured 默认就是 false |
| tags | [] | normalizeTags 默认 |
| source | 原创 | ⚠️ 行为变更(补后显徽标)需知情 |
| commentId | (由 generate-comment-ids.mjs 专职补) | 单一实现、幂等 |
slug 不自动补——中文转拼音的质量不能盲信,只报 ERROR 并提示人工处理。
安全门闩(踩坑后加的):
node scripts/preflight-check.mjs --fix # 只预览:列出将补的字段,不写盘
node scripts/preflight-check.mjs --fix --apply # 真正写盘为什么必须预览门闩:裸 --fix 第一次跑,全库几百篇真实文章被"补齐"到磁盘——虽然每个补全都正确(缺 featured 补 false),但这是发生在"测试"时机的意外批量改动。加了 --apply 门闩后,--fix 单独跑只是列出清单,用户确认才落盘。有副作用的操作,默认只预览。
五、deploy.sh:一键发布的安全设计
发布流程收敛成一个脚本:
bash scripts/deploy.sh1. 前置守卫(目录正确 / node 可用 / pm2 进程存在 / 磁盘空间)
2. 构建前预检 node scripts/preflight-check.mjs # ERROR 阻断
2.5 自动补全 node scripts/generate-comment-ids.mjs && preflight --fix --apply
3. 生产构建 rm -rf .next && npm run build # 防缓存重放
4. 重启 pm2 restart nextjs-jiusi
5. 冒烟验证 curl 关键路由安全设计要点:
set -e+ 每步检查退出码:任何一步失败即停止,不让带病产物活到重启- preflight 放在 build 前:配置/内容问题在编译前暴露,而不是 build 到一半炸
rm -rf .next必须真的在 build 前(系列第 3 篇的缓存坑):注释写了不一定实现,要 grep 确认- 只做"构建→重启→冒烟":不做代码同步(代码靠 scp/rsync 先到位)
六、一次真实的拦截案例
跑 preflight 时它抓出过:
[WARN] categories.yaml: 目录「未分类/站务公告」不在 config 里 → 文章可能不展示
[WARN] categories.yaml: config 里「安全」没有对应目录(磁盘叫「安全技术」)→ 文章将无处挂载这类不一致build 不报错但文章静默消失,只有 preflight 能发现。修复:要么改 config 名对齐磁盘,要么改磁盘目录对齐 config——取决于哪个是"真实意图"。
这就是内容站发布流水线的核心价值:把"内容数据的正确性"从"靠人眼发现"变成"脚本强制校验"。
给普通人的一句话:程序不报错 ≠ 数据是对的;在数据伤害真实用户之前,让一个检查脚本替你把关。
参考
- gray-matter:https://github.com/jonschlinkert/gray-matter
评论
0评论加载中…