几个月没更新的博客,好不容易下定决心重新更新,结果刚升级完没多久,流量就莫名其妙掉了一大截。

本来没太当回事,直到发现后台近 2000 个请求(整整 1,877 次浏览)全都是 404「页面未找到」。抱着试一试的心态用 GA4 排查,没想到这玩意居然还挺强的,一层层维度拆下去,硬是把导致旧文章一夜之间全部失效的根本原因给揪了出来,确实有点意外。

下面记录一下这次完整的排查和定位过程。

1、期望目标

  • 从 GA4 的「页面未找到」聚合报表中,拆解出全部真实的 404 失效 URL 路径和引荐来源。
  • 查明为什么升级后全站旧文章的 URL 会一夜之间集体失效。
  • 将排查出来的 730 条失效地址分类处理(恢复原链接 vs 301 重定向兜底),并建立 URL 冻结机制防止后续升级再次出现问题。

2、计划思考

  • 现状问题
    • 静态博客(Hexo + Butterfly + Cloudflare Pages)平时极少宕机,构建全程正常、监控也是 200,但部分旧 URL 实际上已经 404,没有报警机制。
    • GA4 默认按「网页标题 (Page title)」统计,Butterfly 主题把所有 404 页面统一显示为「页面未找到 | The IT Explorer」,几百个不同文章的 404 访问被合并成了一行,看不出具体是哪篇。
  • 排查思路
      1. 在 GA4 中通过次级维度和探索报表,把具体失效的网页路径和流量来源提取出来。
      1. 对比失效的旧 URL 和线上现有的 URL,看看规则差在哪里。
      1. 根据 404 爆发的时间节点查 git log,定位导致规则变更的代码或依赖。
      1. 对所有失效路径进行分类治理(恢复原规则、301 重定向、归档兜底)。

3、操作步骤

3.1、静态博客为什么容易静默出错

简单来说,我们平时用的静态博客(比如 Hexo、Hugo 等),是把 Markdown 文件编译生成好静态 HTML 页面,然后部署到 Cloudflare Pages 或者服务器上。

这种模式不需要数据库,运行很轻量,但也有个局限:它平时几乎不会宕机,即便链接算错 404 了,构建依然是绿的,服务器也是正常的,所以很难及时察觉

这次在 GA4 里面,最先看到的是整体访问流量的下滑异常:

GA 4 流量趋势概览折线图:8 月初升级后整体流量断崖式下跌与异常波动

升级后总流量断崖式下跌**

打开「网页和屏幕」报表后,默认是按「网页标题」来聚合统计的。Butterfly 主题对于找不到的页面,统一渲染为「页面未找到 | The IT Explorer」。这就导致几百个不同文章的 404 访问,全部被合并到了同一行数据里:

实际发生的情况 GA4 默认报表显示 产生的影响
几百个不同的文章链接返回 404 只有一行「页面未找到」 以为只是零星输错网址
每次 404 都是一次真实访问 1,877 次浏览堆在一起 看不到具体哪篇文章挂了
静态托管服务正常运行 构建与页面状态码正常 故障发生几周都没发现

GA 4 网页和屏幕报表:按网页标题聚合的 1877 次页面未找到 404 错误数据

要定位具体原因,第一步就是把报表拆解到具体路径。

3.2、如何在 GA4 中提取具体的失效链接

既然聚合的标题看不到具体网址,就需要利用 GA4 的维度拆解功能:

步骤 1:添加次级维度快速查看

在 GA4「网页和屏幕」标准报表中:

  1. 找到「页面未找到 | The IT Explorer」这一行;
    添加次级维度

  2. 点击第一列标题旁的 +(添加次级维度)
    网页路径和屏幕类

  3. 选择 「网页路径和屏幕类」(Page path and screen class)
    404 的页面

这样表格就会把每个触发 404 的具体 URL 展开列出来,按访问量排序。

步骤 2:通过探索报表导出完整列表

标准报表只能展开部分,如果需要导出整整几百条的完整清单进行逐条处理:

  1. 点击 GA 4 左侧菜单 「探索」(Explore) → 新建 「空白」 报表;

探索-空白

空白报表

  1. 维度 (Dimensions):点击 + 导入「网页标题」、「网页路径和查询字符串」;

添加网页标题和网页路径维度

  1. 指标 (Metrics):点击 + 导入「浏览次数」;

image.png

  1. 配置行与值:把「网页路径和查询字符串」拖入 行 (Rows),把「浏览次数」拖入 值 (Values)

  2. 设置过滤器 (Filters):添加过滤规则 —— 网页标题 包含 页面未找到 并点击应用。

GA 4 探索报表新建空白模板:导入网页标题、网页路径维度与浏览次数指标

GA 4 自定义探索报表配置:拖拽行与值并设置网页标题包含页面未找到过滤器导出 730 条失效清单

通过这个方法,我导出了整整 730 条不同的失效路径

步骤 3:查看流量来源

在探索报表中把维度换成 「页面引荐来源网址」(Page referrer)「会话源/媒介」

GA 4 失效页面引荐来源分析:查看 404 错误流量的会话源媒介与搜索引擎来源渠道

GA 4 失效页面引荐来源分析:查看 404 错误流量的会话源媒介与搜索引擎来源渠道

可以看到流量主要来自 Google 搜索和外部链接,说明之前积累的搜索引擎收录一直在返回 404,需要尽快恢复。

3.3、比对新旧 URL 找规律

拿到导出的 730 条路径后,把失效的旧链接和线上现存的链接放一起比对:

链接类型 实际路径示例 特征差异
失效的旧 URL(GA4 记录) /posts/20260110-2026-nian-vps-ce-ping-jiao-ben-da-quan…/ 汉字拼音音节之间有连字符 -
线上的新 URL(当前生成) /posts/20260110-2026nian-vpsceping-jiaobendaquan…/ 汉字整体连拼,连字符消失了

从比对中可以明确几点:

  1. 文章都在,只是 URL 的生成规则变了;
  2. 汉字拼音音节之间的连字符(例如 2026-nian-vps-ce-ping 变成了 2026nian-vpsceping)全部没了;
  3. 从时间线上看,这批 404 集中出现在 8 月 1 日 升级之后。

排查流程大致如下:

3.4、为什么连字符会突然消失

有了时间节点和规律,去代码仓库查一下 8 月 1 日当天的提交:

1
git log --oneline --since="2026-07-25" --until="2026-08-05"

很快就定位到了一个提交,当时顺手把 transliteration 这个依赖库做了升级:

1
2
- "transliteration": "1.6.6"
+ "transliteration": "2.6.1"

根本原因说明:

博客的 Markdown 源码采用中文命名(如 20260110 2026年VPS测评脚本大全.md),Hexo 在每次编译生成时,会调用 transliterationslugify 函数把中文转成拼音 URL。

  • 1.6.6 版本slugify('2026年VPS测评') 会分词并加入连字符,输出 2026-nian-vps-ce-ping
  • 2.6.1 版本:内部重构了逻辑,汉字与数字英文字符拼接时不再补连字符,直接输出 2026nian-vpsceping

这就导致全站 151 篇文章(包含 20 个多语言版本)在重新构建后,旧的 URL 全部漂移失效。

另外,Hexo 生成文章路径时会直接使用文件名处理结果,即使在文章的 front-matter 里写了 slug,也容易被覆盖掉。

3.5、这 730 条失效路径如何分类处理

找到原因后,对导出的 730 条路径进行了分类,发现并不全是由这次升级引起的,还有部分历史遗留,一共归为 5 类:

分类 路径示例 数量 产生原因 处理方式
旧拼音格式文章 /posts/20260110-2026-nian-vps-ce-ping…/ 占比最大 8月1日依赖升级导致连字符丢失 恢复原生成规则(直接返回 200),保留 SEO 权重
早期数字短链 /posts/39985/{lang}/posts/{id}.html 8 条 早期使用 abbrlink 插件生成的链接 单条配置 301 重定向至对应文章
多语言标签/分类变体 /de/tags/Ubuntu/ja/tags/VPS退款 约 79 条 多语言标签结构调整后的残留 301 重定向至当前语言的标准标签页
已下架语言目录 /ms/*(马来语整站) 约 42 条 曾经上线过但后续下架的语种 批量 301 规则通配跳转至英文对应页面
空归档月份 /de/archives/2024/02 约 31 条 某个语言在某个月份没有文章 301 重定向至该语言归档总目录兜底

Cloudflare Pages _redirects 配置实现

在静态输出根目录下维护 _redirects 文件:

1
2
3
4
5
6
7
8
9
# 1. 历史数字短链重定向
/posts/39985 /posts/20260110-2026-nian-vps-ce-ping-jiao-ben-da-quan/ 301
/en/posts/39985 /en/posts/20260110-2026-vps-benchmark-scripts/ 301

# 2. 下架语言批量通配跳转
/ms/* /en/:splat 301

# 3. 空归档目录兜底
/de/archives/2024/* /de/archives/ 301

4、总结

其实做独立博客很容易陷入一种心态:越是不更新,就越不想更新。之前脑子里总想着搞个几十篇的宏伟写作计划 独立开发者工具箱:60篇实战指南 ,但只要中间停顿下来,就真的无限期搁置了。好不容易下定决心重新拾掇一下,结果一升级就踩到了这么深的一个坑。

写完的文章老实说平时根本不想回去看,毕竟写的时候太费脑子了。如果这次不是流量掉得太明显、鬼使神差去深挖了一下 GA4,可能这批 404 还会一直静默躺在后台。

这次排查留下的几个核心经验:

  • URL 尽量做静态冻结:已发布文章的 URL 最好作为固定属性存储,不能完全依赖第三方转写库在构建时实时计算,避免依赖升级带来意外漂移。
  • 升级依赖需比对 sitemappackage-lock 只能锁定版本,锁不住依赖包内部的算法行为变更。升级参与路由生成的库之后,最好对比一下前后的 sitemap.xml
  • 能原地恢复的不用 301 将就:核心旧文章通过恢复原生成逻辑直接返回 200,能最大程度保住已有权重与访问体验。
  • 定期查看 GA4 的 404 探索报表:静态博客缺少服务端告警,把「标题包含页面未找到」的探索报表保存下来,定期看一眼,能及早发现问题。

5、异常记录

在 Hexo 中,如果自定义了 URL 处理脚本,应在 hexo.extend.filterbefore_post_render 阶段对已发布文章进行固化拦截:

1
2
3
4
5
6
7
8
# _config.yml
permalink: posts/:title/
permalink_defaults:
lang: ''

pretty_urls:
trailing_index: false
trailing_html: false

2. Cloudflare Pages 重定向配额注意项

  • Cloudflare Pages 单个 _redirects 文件最多支持 2,000 条静态规则 + 100 条动态通配规则(合计 2,100 条),文件体积上限为 100KB。
  • 遇到大批量多语言 404 时,优先使用 :splat 通配符做目录级收拢,避免逐条穷举写满配额。