把发布链路当成一个可验证的系统
博客写久了,真正让人疲惫的往往不是写作,而是发布:图片放在哪个目录、元数据有没有写全、文章里有没有不该出现的个人信息、构建失败后站点会不会停在半坏的状态。任何一步靠“记得”,迟早会出事。
这次我把整条链路收口成一个可以反复验证的系统:客户端只负责产出标准包,云端只接受符合契约的包,敏感信息检查放在两个位置,发布失败必须能回滚。
两层结构
链路被拆成两个角色。
客户端是打包端。它不持有云端权限,也不负责上传,只做三件事:读取 Markdown、把散落在各处的本地图片收进标准目录、生成一个自检过的压缩包。
云端是服务端。它负责解包、校验、图片上传、写文章、构建、切换和验证。客户端通过检查,不代表云端一定通过;云端会用同一套规则独立复检。
这种拆分的好处是权限边界清晰:客户端 AI 可以随意使用,上传只发生在被授权的机器上。
契约先行
最早的实现是“尽量兼容”:md 找不到就递归找,元数据缺了就回退到 frontmatter,图片路径写成什么样都试着解析。看起来宽容,实际是把不确定性留给了线上。
现在的规则是硬的:
- 包根只允许一个 Markdown、一个
images/目录、一个_meta.json; - Markdown 必须叫
<slug>.md,不能有第二个; _meta.json必填,字段类型、长度、时区都要对,未知字段直接拒绝;- 图片只能放在
images/下,扁平,不允许子目录; - 本地图片引用必须写成
images/<文件名>,引用的文件必须存在。
这套规则不是为了刁难作者,而是为了让失败发生在打包时,而不是发生在读者打开页面之后。
图片不是附件,是输入
图片最容易出问题。路径写错、文件被截断、扩展名和内容不一致,都会让页面出现空白或裂图。
客户端现在会做几件事:
- 本地图片可以在任意相对路径,打包时统一复制进
images/; - 按文件内容去重,同一张图只保留一份;
- 同名但内容不同的图片加短哈希后缀,避免互相覆盖;
- 检查文件是否完整:PNG 要有结束块,JPEG 要有结束标记,GIF 要有 trailer,WEBP 的长度要自洽,SVG 必须是合法 XML;
- 内容与扩展名不一致、空文件、无法识别的格式,直接失败。
云端还会再查一遍 magic bytes,并对上传结果做 HEAD 校验。上传失败时回退到站点自身托管,保证页面至少可访问。
敏感信息检查:OCR 只做眼睛
另一个容易被忽略的问题是图片里的文字。截图、聊天记录、后台页面,都可能把手机号、证件号、内网地址或密钥带进文章。
现在的做法是:云端用 OCR 把图片文字识别出来,但判定完全在本地脚本完成。规则是正则加校验位,不把识别结果交给模型去“感觉”。
检查范围包括 Markdown 正文、元数据字段和每张图片。命中分两级:
block:证件号、手机号、银行卡号、内网地址、云密钥、令牌、私钥、口令赋值等,整包拒绝;warn:邮箱、地址、姓名、统一社会信用代码等,打印提示后继续。
所有输出都做脱敏,只显示掩码。OCR 结果不落原文,缓存里只保留哈希和脱敏后的命中项。
这套检查不能替代人工判断,但能挡住大部分“顺手截图”的事故。
发布与回滚

发布不是把文件拷过去就完了。云端会先构建,构建失败时线上完全不动;构建成功后先暂存,再切换目录;切换后验证首页、RSS 和本次发布的文章,任何一项不是 200 就恢复最近的备份。
备份目录保留时间戳,出问题时可以手动回滚。发布成功时输出站点地址和备份路径,失败时输出具体是哪一项验证没过。
结果
现在,一篇普通 Markdown 只需要提供标题、摘要和标签,就可以完成规范化、打包、校验、上传、发布和验收。图片在哪个目录不重要,网络图片保持原样,本地图片会被收进标准结构。
更重要的是,失败的位置提前了:以前是读者看到裂图,现在是打包命令直接报错;以前是发布后才发现页面打不开,现在是验证阶段自动回滚。
边界
这套系统仍然有边界:OCR 只能看文字,识别人脸、二维码内容或纯图形不是它的能力;规则可能误报,也可能漏报;发布链路没有并发锁,不能同时跑两个导入任务。
但在“可验证”这件事上,它已经比“记得检查”可靠得多。工程上的安全感,通常不是来自更多的功能,而是来自更少的意外。