两年前 _redirects 只有 40 行,现在已经 3200 行了。每次 slug 改名、每次分类重整、每次发布后才发现的拼写错误,都加一行,从来没人删。结果构建多花 8 秒解析文件,边缘函数冷启动撞到内存上限,Search Console 里 “Page with redirect” 桶越涨越大——因为 Googlebot 在追 3 跳链路,最后一跳还是 410。重定向表本身已经变成了一笔技术债。
这篇文章讲清楚膨胀是怎么发生的、怎么安全审计和压平,以及哪些规则能防止它再次堆起来。
常见原因
按命中率从高到低排列。
1. 每次改 slug 都加了一条”向前”规则,从来没合并过
编辑把 /why-claude-is-better/ 改成 /claude-vs-gpt-comparison/,加了一条重定向。半年后又改成 /claude-4-vs-gpt-5/,又加一条。现在变成 3 跳:第 1 跳 → 第 2 跳 → 第 3 跳 → 200。
怎么判断:挑几个源 URL 跑 curl -sIL,如果最终 200 之前出现多个 Location 头,就是链路在跳。
2. 一次分类大改一次塞了几百条
把 /category/ai-tools/ 改成 /ai-applications/,这个 PR 一口气加了 400 条重定向,覆盖所有子 URL,但没有一条收成通配符。
怎么判断:grep -c "^/category/ai-tools" _redirects。如果数量在几百,而且每一行都指向 /ai-applications/ 下的对应子路径,那一条通配符就能取代。
3. 给 Google 早就忘掉的 URL 写的重定向
2023 年给一个 12 次访问、18 个月没被爬过的 URL 加了重定向。规则到今天还在每次部署里。
怎么判断:把重定向源路径跟过去 12 个月的 Search Console + 站内分析交叉对比。0 曝光 + 0 点击的源就是死重量。
4. 斜杠、www、协议归一化全塞在重定向表里
/foo、/foo/、http://、https://、www.、非 www. 每个组合都给每个页面单独写了重定向行。等于每个 URL 6 条规则,本该在 host 层一次解决。
怎么判断:同一个目标 URL 出现 4-6 次,源只差斜杠/host。
5. CMS 迁移时遗留下来、没人认领的旧规则
从 WordPress 迁出来时,把整个重定向插件的数据库导出来塞进去了。600 条”上一代上一代 CMS”的 URL,过去 5 年没有任何链接指向它们。
怎么判断:源路径不符合当前 URL 结构(/?p=1234、/index.php?cat=...、/wp-content/...)。
6. 重复规则,目标还不一样
同一个源路径出现两次,目标不一样。哪一条生效跟平台有关(Netlify 取首条,Cloudflare Workers 取末条)。
怎么判断:awk '{print $1}' _redirects | sort | uniq -d。
7. 软 404:全部重定向到首页
页面删了,有人把它 301 到 /。结果几百个毫不相关的 URL 都跳到首页。Google 把这些识别成软 404,在 Search Console 里报”Submitted URL seems to be a Soft 404”。
怎么判断:grep -E " / 30[12]$" _redirects | wc -l,如果不只是个位数,就是模式不是例外。
最短修复路径
第 1 步:快照,版本化保留
动手前先把 _redirects 拷一份带日期的备份,方便对比和回滚:
cp public/_redirects public/_redirects.snapshot-$(date +%Y%m%d).txt
git add public/_redirects.snapshot-*.txt
git commit -m "snapshot: redirect map before audit"
第 2 步:把所有规则解析到最终目标
把每条重定向一路追到底,写成 TSV。这是整个清理过程里最有用的一份产物。
// scripts/resolve-redirects.mjs
import fs from 'node:fs';
const lines = fs.readFileSync('public/_redirects', 'utf8').split('\n');
const map = new Map();
for (const line of lines) {
const m = line.match(/^(\S+)\s+(\S+)\s+(\d+)/);
if (m) map.set(m[1], { to: m[2], code: m[3] });
}
function resolve(path, hops = 0) {
if (hops > 10) return { final: path, hops, loop: true };
const rule = map.get(path);
if (!rule) return { final: path, hops };
return resolve(rule.to, hops + 1);
}
for (const [from] of map) {
const r = resolve(from);
process.stdout.write(`${from}\t${r.final}\t${r.hops}\t${r.loop ? 'LOOP' : ''}\n`);
}
任何 hops > 1 都是要压平的链路,任何 LOOP 是 bug,先修。
第 3 步:一次性把链路压平
把每条多跳规则直接改成指向最终目标。一般能砍掉 20-30% 的爬虫可见延迟,顺带把”链路尾巴是已删页面”造成的软 404 风险一起消掉。
第 4 步:批量规则改成通配符
主流边缘平台都支持通配。400 条同级重定向可以收成一条:
# 改之前
/category/ai-tools/chatgpt /ai-applications/chatgpt 301
/category/ai-tools/claude /ai-applications/claude 301
# ... 还有 398 条
# 改之后
/category/ai-tools/* /ai-applications/:splat 301
删原始规则之前,先用 curl -sI 抽 5-10 个样本验一下。
第 5 步:0 流量 + 0 内链的规则直接淘汰
把重定向源跟以下四类信号对比:
- Search Console:12 个月内有曝光吗?
- 站内分析:12 个月内有会话吗?
- 内链:站内还有任何地方链向这个 URL 吗?
- 外链:外链工具里还有引荐来源吗?
四项全是”否”,且规则超过 12 个月,直接删。这个 URL 已经从可索引网络里消失了。
第 6 步:把归一化挪出重定向表
斜杠、host、协议这种归一化应该在边缘配置或框架配置里做,不是逐页写规则。Astro 里:
// astro.config.mjs
export default defineConfig({
trailingSlash: 'always',
site: 'https://example.com',
});
然后把 _redirects 里每一条逐页的斜杠变体都删掉。
第 7 步:加上行数上限和”有效期”
CI 加一道检查:_redirects 超过 N 行、或包含超过 M 个月没注释说明的规则,就让构建失败。
// scripts/check-redirects.mjs
const MAX_LINES = 800;
const lines = fs.readFileSync('public/_redirects', 'utf8').split('\n').filter(Boolean);
if (lines.length > MAX_LINES) {
console.error(`Redirect map has ${lines.length} lines (cap: ${MAX_LINES})`);
process.exit(1);
}
配合一条注释约定:每条规则上方必须有 # added: YYYY-MM-DD reason: ...。每年一次例行审计,把陈旧的清掉。
哪些不该算在你头上
重定向表本质是过去若干年决策的账本。一部分膨胀是合法 URL 卫生的代价。目标不是”零规则”,而是”可审计且无链路”。如果业务确实四次重命名了大栏目,你就会有几百条规则,这没问题,只要每条链路已经压平、每条规则都有理由。
容易误判成
- “构建慢是 MDX 的锅。“——其实重定向表解析往往才是热点,先 profile 再下结论。
- “内容页爬取预算不够。“——Googlebot 把预算花在你的重定向表上,不在新内容上。
- “边缘函数内存上限要升级。“——如果函数冷启动时把
_redirects整个加载到内存,缩文件比升运行时便宜得多。
预防
- 加新重定向的同一个 PR 里,把多跳链路压平(写个脚本,不是流程靠人记)。
- 分类批量改名一律用通配符,不要手工枚举同级。
- 每条规则注释
# added: YYYY-MM-DD reason:,以后才知道能不能删。 - 季度任务:把超过 12 个月、0 曝光、0 点击、0 内链、0 外链的规则全删。
- 协议/host/斜杠的归一化挪到边缘配置,不要逐页写。
- CI 加行数上限,文件不能悄悄涨过阈值。
FAQ
- 删旧重定向会不会伤 SEO? 只有当源 URL 还有曝光、内链、外链或者点击时才会。四个信号 12 个月都是 0 的,这个 URL 已经不在可索引网络里了,规则就是死重量。
- slug 改名用 301 还是 302? 长期保留的改名用 301。302 只在真临时移动时用,内容站很少遇到。