给 URL 改名,而不丢流量
301 是好办的那半。难办的是知道哪些 URL 消失了——这需要一本只增不删的台账,记下你真正上线过什么;需要一道读构建产物、而不是读 sitemap 的闸门;还需要把重定向写成一种不会静默失败的形式。方法、清单、台账格式,以及我们矩阵里失败的三种来源与目标形式——其中两种在解析期被点名,一种在任何一层都不出声。
本页如何生产
由 AI agent 起草。 由与生产方不同模型族的模型逐条对照来源与原始数据做了对抗式审查,无未决 P0。尚未经人工通读。 证据最近核验于 2026-09-09。
写那条 301 从来不是难的部分。难的是察觉你欠了它一条。
每一份讲 URL 改名的指南都跟你说同一句话:从旧地址往新地址返回一条 301。这条建议正确、 完整,而且几乎没用——因为它起手的前提你根本满足不了。它假定你知道哪些 URL 消失了。
静态站点上,你通常不知道。一个路由文件改了名,一个 slug 顺手理顺了,一个语种目录重排
了,构建通过,diff 里只显示某个源文件移动过。没有任何地方写着*「地址 /en/old-thing/
已经不存在了」*。404 是几周后才到的,躺在一份没人打开过的爬虫报告里。
一句话结论:长期的解法不是重定向,是一本台账——只增不删地记下你上线过的每一个 URL,放在产出这些 URL 的内容之外,再配一道闸门,每次构建都拿它去对构建产物。
谁该用它——谁该跳过
适合你,如果:
- 你的站点是静态导出、以文件形式部署的,「我们发布了什么」就是一个能遍历的目录;
- URL 会变,不是从不变——新栏目、改名的 slug、合并的页面;
- 你已经有一套会替你变红的构建,或者想要一套。
跳过它,如果:
- 什么都还没被收录——先发布,从第一次真实部署开始记台账;
- 你的平台已经有一份带变更检测的、长期有效的路由登记表;
- 你的 URL 是请求时从数据库里取的——台账这个思路仍然成立,但枚举那一步是一次查询, 不是遍历目录。
改名出错的三种方式
| 失败 | 长什么样 | 明显的修法为什么漏掉它 |
|---|---|---|
| 静默消失 | 旧 URL 404,几周没人察觉 | diff 里没有任何东西说某个 URL 没了——只显示某个文件移动过 |
| 301 落进 404 | 重定向触发了,目标不存在 | 规则照着计划写,没拿构建产物核过 |
| 静默的死规则 | 规则写了、部署了、被计入有效,一次都没匹配上 | 平台能收下这个语法,然后一声不吭 |
三种要三种不同的防线,而大多数建议只讲第一种。
方法
第 0 步 —— 在你需要它之前,先把台账建起来
第一次改名之前,先记下你当前发布的东西。我们那本就是一份纯文本文件,站内相对路径,一行 一条,闸门上线那天照着真实构建产物录的:39 条 URL——36 个页面加 3 个订阅源。
有两个性质比格式更重要:
- 站内相对,不是绝对。 我们打算改一个环境变量,就把这套构建复用到第二个源站上; 绝对 URL 的台账,那天一到就作废。
- 只增不删。 某个 URL 第一次发布时添一行,此后永不删除。删掉一行等于解除一项义务, 这应该是一次看得见、有人能站出来反对的 diff——而不是某个脚本的副作用。
第 1 步 —— 把「已发布」的定义从构建产物上推出来,只推一次
台账的好坏,取决于已发布这个定义。有两样东西要用它——检查它的那道闸门,和往里追加的 那个工具——同一个定义实现两遍就会漂移。定义一漂移,不会大声报错,它会凭空造出根本没 发生过的消失。我们那份住在一个模块里,两边都 import 它。
这个定义本身还扛着第二个承重决定:
「已发布」的意思是「存在于构建产物里」,不是「出现在 sitemap 里」。 一个页面可以 离开 sitemap 却照样返回 200。在我们站上,回落页——canonical 指向另一语种的那种—— 是有意排除在 sitemap 之外的,同时照常响应请求。它不欠任何人一条重定向。拿 sitemap 成员资格当判据,你会在恰恰工作正常的那批页面上,造出一堆幽灵消失。
第 2 步 —— 把重定向写成真正会生效的形式
第三种失败模式就住在这里,而这一种你靠推理绕不过去——只能实测。在我们的托管方那里,
_redirects 是一个
纯文本文件,这让人以为规则要么生效、要么报错。都不是。
我们在本地 Pages 模拟器(wrangler 4.65.0)上跑了一次实验——一张 48 种规则形式、 每种请求 3 次、共 144 次请求的矩阵,另加四组事先声明的对照组,全部通过。有两种来源 形式一次都没生效:
| 来源形式 | 结果 | 有警告吗? |
|---|---|---|
相对路径 /a/ 或 splat 通配 /a/* | 生效——24 条规则里 22 条;另外 2 条挂在它们的目标上,不是来源 | ——(那 2 条在解析期被点了名) |
绝对 URL https://host/a/ | 12 条规则、36 次请求,全部 404 | 有——解析期带行号点名 |
协议相对 //host/a/ | 12 条规则、36 次请求,全部 404 | 没有——这 12 条里有 11 条在任何一层都没产生信号 |
这些结果说的是规则来源。目标有它自己的失败方式:在我们的跑测里,一条状态码写 200
的跨域目标没有做代理转发——请求落到了 404,解析器把它点了出来。目标那张表在本页末尾
那份资产里。
协议相对这种形式最危险。它以 / 开头,所以能通过「必须是相对路径」的检查——而在我们的
跑测里,它被计入有效规则,从未报警,也从未匹配上。写了,收下了,一声不吭,是死的。
范围限制,写在这里,不放进脚注:那些数字来自本地模拟器,不是生产环境的边缘节点。 我们不断言生产边缘的行为与此相同。但即便如此也该避开这几种形式,理由不在那次测量—— 把一次迁移押在厂商文档没有规定的行为上,无论测量结果朝哪边,都是笔坏买卖。
第 3 步 —— 重新构建,让闸门去找消失
接下来是机械的部分。每次构建,遍历台账:
- 还在构建产物里吗? 不欠任何东西,继续。
- 没了? 那就必须有一条对得上的重定向规则。
- 这条规则必须是 301,不是 302——改名是永久的,而临时重定向等于告诉爬虫继续用 旧地址。
- 目标必须存在于构建产物里,否则你上线了一条落进 404 的 301。(有意指向跨域目标的 那一条例外:构建没法检查另一个源站,这种情况按「已声明」接受。)
新发布的 URL 不是错误。 台账允许落后于站点;只有消失却没有 301 才算失败。我们的 闸门每次构建都打印未结清的数量,让这份落后一直看得见,而不是变成隐形欠债。
第 4 步 —— 追加是一次单独的、刻意的动作
往台账里加东西,是一条由人执行的命令,刻意不进构建。理由很短:
一道会改写自己期望值的验证器,只会越跑越绿。
如果构建既检查台账又更新台账,那每次改名都成了自己批准自己。所以:构建只读,人负责写。 追加这一步只增行,而且一旦构建产物枚举出零条 URL 就干脆拒绝运行——因为一个坏掉的遍历 器,看起来跟一个什么都没发布的站点一模一样。
第 5 步 —— 两份清单,别混
我们维护两份 URL 清单,混为一谈会把两份都毁掉:
| 冻结的历史 | 现役台账 | |
|---|---|---|
| 装什么 | 某一次具体迁移之前的 12 条 URL | 上线过的每一条 URL;起底时 39 条,之后还在长 |
| 会变吗? | 从不 | 只增不减 |
| 从哪儿来 | 一份当时抓下来的基线 sitemap,另加手工补的 2 条订阅源 URL | 真实的构建产物 |
两份都不从内容目录推导,而对冻结那份来说,这条规矩是有牙齿的:迁移之后才建出来的页面, 从来没有过一个旧 URL,所以拿当前内容去推「旧 URL」清单,等于让这道检查断言今天的页面 重定向到自己身上。同义反复,穿了件闸门的衣服。
第 6 步 —— 证明闸门还咬得动
闸门变绿只说明没有东西变红,不说明它还在盯着。在信它之前,我们先把它故意弄坏—— 四个状态转换加三组对照组:
| 注入的状态 | 预期 | 实测 |
|---|---|---|
| 删掉一个已发布的页面,不加规则 | 红 | 红——点出了那个 URL 和缺失的规则 |
| 把规则加成 302 | 红 | 红——「改名必须是永久的 301」 |
| 把 301 指向一个不存在的目标 | 红 | 红——「一条落进 404 的 301」 |
| 把 301 指向一个真实存在的目标 | #23 变绿 | 绿 |
| 从台账里删掉一行(新页面,尚未记录) | 通过并提示 | 通过了,并打印了那条提示 |
| 把台账清空 | 红 | 红——「下面所有检查都会空转通过」 |
| 把台账改写成另一个站点的路径 | 红 | 红——「与构建产物共享 0 条 URL」 |
最后两条是防空转的对照组,它们比看上去更重要:一道遍历空清单的检查,通过得完美无缺, 什么也没守住。
那次跑测里有个细节,如实交代。 我们打上正确的修复之后,被测的那道闸门变绿了——而 整轮运行仍然以非零退出,因为另一道检查红了:把一个页面移出构建产物,破坏了 sitemap 与页面数量之间的一条不变量。我们最初的记录凭推断把这次失败归到了错的检查头上。重跑一遍、 把完整输出读完,才纠正过来。我们从中得到的规矩,也正是这一整页所依赖的那条:先读 输出,再归因——绝不能倒过来。
我们核验了什么——没核验什么
- 已核验:闸门、共用的定义、只增不删的工具,以及那两份 URL 清单,都正在这个仓库里 跑;上面七个注入状态产出的就是表里那些结果;重定向形式矩阵是真跑过的,它的四组对照组 事先声明、全部通过。
- 未核验:我们的台账到底有没有抓到过一次意外的改名。我们给它看过的每一次失败, 都是自己注入的。它从 2026-09-09 跑到现在。
- 未核验:任何重定向形式在生产环境边缘节点的行为。一个本地模拟器,一次实验,没有 独立复现。
- 不归我们核验:搜索引擎会不会通过某条重定向传递你想要的那个信号。以下面来源里的 平台文档为准;我们的闸门只保证重定向存在、是永久的,并且落在一个真实存在的地方。
局限
台账守的是地址,不是含义——把一个页面挪走再掏空,这里每一道检查照样通过。它假定 构建产物可以遍历。它的重定向结论来自一次本地跑测。而且到目前为止,它抓到的全是我们 自己埋的故障。
把资产拿走
| 资产 | 它是什么 |
|---|---|
| 改名清单 | 16 道二元闸门:改名之前、写重定向、构建之后,另加两条讲纪律的 |
| URL 台账格式 | 文件格式、追加规则,以及要拿它跑的四种检查 |
| 静默失败的重定向形式 | 实测出来的矩阵、哪些报了警哪些没有,以及范围限制 |
下一个动作
第 0 步今天就做,趁你还没需要它。把你构建当前发布的东西枚举出来,把这份清单提交进版本 库,再写下那一句为你的站点定义「已发布」的话。那个文件一旦存在,这一页剩下的都是机械 动作——而一旦第一个 URL 已经消失,你再想诚实地补上它就不可能了。
来源
Google
Redirects and Google Search — Google Search Central documentationdevelopers.google.com · 核查于 2026-09-09
Google
Site Moves and Migrations — Google Search Central documentationdevelopers.google.com · 核查于 2026-09-09
Cloudflare
Cloudflare Pages — redirects (_redirects syntax, splat semantics, limits)developers.cloudflare.com · 核查于 2026-09-09