博客运维实录:两篇"被吞"的文章与一次全站体检


博客复更两个月,文章攒到 95 篇,决定做一次例行体检。结果:两篇线上文章实际只渲染了前三分之二,存在了很久,从没人发现。这篇记录完整的排查过程——最值钱的一段是”markdown 渲染器直渲完整、hexo 管线却吞内容”的定位方法——以及体检脚本的设计、顺手的几项优化。同类用 Hexo + 老内容迁移的博客大概率会撞上同样的问题。

一、故障:看起来完全正常的截断

最初的线索来自一次顺手改动:批量调整 SpringBootNotice 这篇的 front-matter 时,发现源文件 3931 行,而渲染页的内容在”7)、CRUD-员工修改”之后就没了——后 43% 凭空消失。

诡异的是这篇文章的表象:前三分之二排版完全正常,标题、代码块、高亮一应俱全;目录只显示存在的部分,也没有任何构建报错。读者不会觉得少了什么——除非恰好知道原文还有 Docker、数据访问、启动配置原理整整五章。

同批体检还揪出另一篇:KnowledgeGraphs 的正文里拼接了三份内容(一份截断 + 两份完整),线上页面正把垃圾 front-matter 渲染给读者。这篇是上次写作时拼接脚本出错留下的伤,同样无人察觉。

两件事暴露的是同一个盲区:截断是”静默故障”——页面看起来正常,只有对照源文件才能发现。靠偶然改动撞见一次是运气,不是机制。

二、排查:一层一层隔离渲染管线

定位过程的教训浓缩成一句话:先隔离层,再找行。

第一步:渲染器直渲,排除 marked

hexo 的 markdown 渲染走 hexo-renderer-marked(内部是 marked 4.0.18)。绕开 hexo,用 marked 直接渲染全文:

const marked = require('marked');
const out = marked.parse(body); // 131,784 字符
console.log(out.includes('桥接网络'));      // true(尾段内容)
console.log(out.includes('事件监听机制')); // true

直渲完整。排除了 markdown 解析器本身,嫌疑收敛到 hexo 在渲染器之外做的那一层。

第二步:模型层复现

用 hexo 自己的模型加载文章,看渲染缓存里的内容:

const Hexo = require('hexo');
const hexo = new Hexo(process.cwd(), {silent: true});
await hexo.init(); await hexo.load();
const post = hexo.model('Post').findOne({title: /SpringBoot核心/});
post.content.includes('桥接网络'); // false —— 复现

模型层复现了截断。而且 content 长度 240,600 字符,比 marked 直出的 131K 还大 109K——里面除了被吞的尾段,还有别的异常。到这里可以确定:问题在 hexo 的内容处理链(渲染前钩子或渲染后的模板引擎层),而不是 markdown 语法。

第三步:后缀二分,圈出 20 行窗口

对全文逐行二分效率太低,换了个思路:从文件尾部往前找”从第 K 行开始渲染就完整”的最小 K——判定函数是”渲染结果里包含尾段的两个标志性标题”。二分收敛后,病灶锁定在一个 20 行的窗口:员工列表表格那段 Thymeleaf 模板代码。

第四步:字符级检查,元凶现形

对窗口逐行做模式扫描,命中两个高危序列:

  • L2220:th:value="${emp!=null}?${#dates.format(...)}"
  • L2235:th:text="${#dates.format(...)}"

元凶是 {#——Thymeleaf 表达式里毫无恶意的两个字符。但 hexo 渲染完 markdown 之后,会把内容交给模板引擎处理,而 Nunjucks(hexo 用的模板语言)里 {# 正是注释的开始符。全文件扫描确认:这两处 {# 之后,没有任何 #} 闭合。

于是发生的事情是:模板层把从第一个 {# 开始的全部内容——1700 行——当成了注释,整个吞掉。L2220 在文件里的位置,和渲染截断点的位置,严丝合缝。

修复:中和而非删除

直接删掉 #dates 会改变文章内容,不可接受。修法利用了 HTML 实体的一个特性:

{#  →  {#

# 是 # 的数字实体,浏览器渲染后读者看到的仍是 {#dates...} 原样;但模板层读到的是 {#,不再触发注释。同理,MathjaxTutorial 里 \color{#FF0} 的公式也中招了(它的公式保护脚本把公式摘出再放回,恰好让一处 {# 漏过了保护),同法中和后 MathJax 读到的 LaTeX 不变,颜色公式照常渲染。

修复后验证:SpringBootNotice 的尾段五章全部回归,MathjaxTutorial 末节”一些特殊的注意事项”出现在渲染页。

三、为什么拖了这么久才发现

复盘这个故障的存活时间(SpringBootNotice 的截断至少存在了几年),原因有三条,每条都值得记住:

静默失败。模板层吞内容不报错、构建不告警、页面不空白——它只是安静地少渲染一大段。hexo 的构建日志里一个字都不会提。

目录不对照。主题的 TOC 从渲染后的标题生成,被吞的章节根本不在标题列表里,目录”完整”地显示了不完整的内容。

读者沉默。从搜索进来的读者不知道原文有八章,读到第五章结束就走了,没有反馈渠道会提示”这篇少了 43%”。

同样的静默模式还有一次变体:KnowledgeGraphs 的三份拼接不吞内容,反而是多渲染——读者读完全文会再撞见一遍全文。同样是”看起来正常”。

四、体检脚本:把撞运气变成跑命令

两颗雷都是手工排查挖出来的,但手工不能复用。把这轮的经验固化成 scripts/content_check.py,五类检查:

检查项 抓什么 来源
零宽字符 U+200B/200C/200D/FEFF 残留 语雀粘贴的行首杂质(全站清了 13 处)
围栏配对 代码围栏标记条数为奇数 = 未闭合 老文常见
模板危险序列 模板层的注释/变量/标签开始序列(任何位置,含代码块内) 本次的直接元凶
渲染截断 源文件末标题是否出现在渲染页 静默故障的机器判定
重复内容 相同标题 + 相同内容块出现两次 拼接事故特征

其中”渲染截断”检查的原理就是这次排查的思路反向利用:拿源文件的最后一个标题当探针,去渲染页里找;找不到就是截断。它要求先 hexo generate,之后对全站逐篇比对,几分钟跑完全站。

第一轮跑出来的结果也说明了误报处理的重要:初版报了 21 篇,其中”类图×2””安装×2”这类标题重复是设计模式系列文章的正常结构(一个模式一节类图)。收紧判定——只在”相同标题且后接相同内容块”时才算事故——之后剩 4 篇,逐一核对后全是扫描器自身缺陷的假阳性(围栏奇偶反转导致的行号漂移)。体检工具的误报率决定它还能不能被继续使用,这一步值得花时间。

五、顺手做掉的一串

体检过程中暴露的相关问题,一并处理:

  • giscus 评论系统上线:基于 GitHub Discussions,文章页与留言板都挂了,留言板终于不是空页
  • 图床兜底:jsDelivr 主域失败自动降级 fastly → testingcf → gcore → raw.githubusercontent,四链兜底只管正文 <img>
  • 全站图片懒加载:渲染后滤镜给正文 <img> 补 loading="lazy",长文几十张图的流量大头立省
  • 大图 WebP 化:11 张 >500K 的图转 WebP,13.9M → 5.4M(-61%),带文字的图走无损、照片走 q88
  • robots.txt 与 RSS 全文:robots 原来根本不存在;atom 被插件默认的 20 条上限截着,放开到 100 条全文
  • 站点 meta 更新:description 还是 2022 年的”正在学习编程的小猿”,已换成与内容版图一致的文案

六、经验收束

四条可以带走的:

渲染问题先隔离层。”渲染器直渲正常、管线异常”这一个对照,就把搜索空间砍掉了一半;剩下的用二分收敛窗口,比通读代码快得多。

老内容是格式债。凡是从语雀/印象笔记/CSDN 编辑器粘贴过来的内容,零宽字符、未闭合的模板序列、半截 HTML 都是潜伏资产——它们在自己原来的平台无害,换一个处理管线就可能引爆。迁移内容时做一次格式消毒,比事后排查便宜一个量级。

“看起来正常”是最危险的故障模式。报错页面会被立刻处理,静默截断存活几年。任何”内容完整性”的保障都不能依赖目测——目录、正文、源文件三者对不上就是故障,哪怕页面再漂亮。

体检必须可执行化。这次的经验若只写成文章(比如你现在读的这篇),下个季度就会走样;固化成 python3 scripts/content_check.py 一条命令,才成为发布流程的一部分。

参考


文章作者: Halensea
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Halensea !
  留言
 本篇
博客运维实录:两篇"被吞"的文章与一次全站体检 博客运维实录:两篇"被吞"的文章与一次全站体检
博客复更后的例行体检挖出两篇线上文章只剩三分之二——存在了很久没人发现。排查的结论很有意思:markdown 渲染器直渲全文完整,问题出在 hexo 管线的模板层,元凶是正文里 Thymeleaf 表达式撞上模板注释开始序列。这篇记录完整的定位过程、体检脚本的五类检查,以及这轮运维顺手做掉的一串优化。
2026-11-10
下一篇 
Whispering Agents 解读:Agent 互联网的隐蔽通信协议 Whispering Agents 解读:Agent 互联网的隐蔽通信协议
Agent 之间的消息加密之后,"谁在跟谁通信、几点通信、通信了多少次"仍然全在明面上——这是流量分析,加密管不了。北邮团队在 AAAI 2026 发表的 Whispering Agents 把 Agent 的每次公开交互当成载体,同时调节内容、时间和行为三个维度,让秘密对话藏进正常寒暄里;训练过的检测器 AUC 只有 0.503,接近瞎猜。
  目录