本文约 3,521 字,预计阅读 9 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:改版后流量腰斩,问题出在跳转表
站点改版更换固定链接结构是最常见的流量事故场景。有人把 /2023/05/hello-world.html 换成 /blog/hello-world/,上线时只配了首页和几个栏目的跳转,剩下几千篇文章直接 404。等 GSC 报告出来,自然流量已经掉了六成,外链带来的权重全部漏光。
批量重定向的难点从来不是”写一条跳转”,而是三件事:如何完整拿到旧 URL 清单、如何建立可信的映射关系、如何在上线前验证没有链条和循环。这三步缺任何一步,上线就是赌运气。
原理分析:映射关系从哪来
旧 URL 清单有四个来源,优先级从高到低:
| 来源 | 覆盖度 | 说明 |
|---|---|---|
| GSC”网页”报告已索引 URL | 高 | 必做,直接给出被收录的地址 |
| GSC”链接 → 外部链接”报告 | 中 | 找出有外链的页面,权重最关键 |
| GA4 落地页历史数据 | 中 | 补充有流量但未被索引的页面 |
| 数据库历史数据 | 高 | 用于生成 slug 映射 |
映射关系的建立有两种思路:规则型(旧结构可通过正则推导出新结构)和枚举型(新旧 slug 无规律,必须一对一列举)。规则型优先,配置简洁、性能最好;枚举型用 map 实现,注意条目过多时要评估内存占用。
关于跳转链:A→B→C 的链条会让信号在每一跳衰减,同时增加 TTFB。理想状态是任何旧 URL 一次跳到最终地址。循环则是致命错误,浏览器直接报 ERR_TOO_MANY_REDIRECTS,页面彻底不可访问。
保姆级实操
步骤 1:导出旧 URL 与外链清单。 先从 GSC 导出”网页”报告全部 URL,再导出”链接”报告中的”链接最多的网页”,合并去重后作为必须跳转的集合。
步骤 2:从数据库生成 slug 映射。 若只是改了固定链接结构,文章 ID 与 slug 不变,可直接生成映射文本。
-- 按旧结构(/%year%/%monthnum%/%postname%.html)生成旧->新映射
SELECT CONCAT('https://example.com/', DATE_FORMAT(post_date, '%Y/%m'), '/', post_name, '.html') AS old_url,
CONCAT('https://example.com/blog/', post_name, '/') AS new_url
FROM wp_posts
WHERE post_type = 'post'
AND post_status = 'publish'
ORDER BY post_date DESC;
导出为 TSV 后,直接转成服务器配置。
步骤 3:规则型跳转用 Nginx 正则。 结构可推导时,几条规则就能覆盖全部。
# 规则型:旧日期型链接 → 新 slug 链接
location ~ ^/(\d{4})/(\d{2})/([^/]+)\.html$ {
return 301 https://example.com/blog/$3/;
}
步骤 4:枚举型跳转用 Nginx map。 无规律的一对一映射用 map 集中管理,注意把 map 放在 http 块中。
# 放在 http {} 块内
map $request_uri $redirect_to {
default "";
/old-about /about/;
/products/item-12 /shop/item-12/;
/news/2024/notice /blog/2024-notice/;
}
# 放在 server {} 块内
if ($redirect_to != "") {
return 301 https://example.com$redirect_to;
}
map 的键区分大小写,默认不区分查询参数;若旧 URL 带参数,需要先用 $uri 而非 $request_uri。
步骤 5:上线前批量验证。 写个脚本把映射表逐条跑一遍,检查状态码、跳转次数和最终地址是否符合预期。
#!/usr/bin/env bash
# verify_redirects.sh 用法: ./verify_redirects.sh map.tsv
# map.tsv 每行格式: 旧URL<TAB>期望目标URL
while IFS=$'\t' read -r old want; do
got=$(curl -sIL -o /dev/null -w "%{http_code} %{url_effective} %{num_redirects}" "$old")
code=$(echo "$got" | awk '{print $1}')
final=$(echo "$got" | awk '{print $2}')
hops=$(echo "$got" | awk '{print $3}')
if [ "$final" != "$want" ] || [ "$hops" != "1" ]; then
echo "异常: $old -> 状态码 $code, 跳 $hops 次, 最终 $final (期望 $want)"
fi
done < "$1"
echo "验证完成"
脚本会同时暴露两类问题:跳转目标不对、跳转次数大于 1(存在链条)。
步骤 6:检测循环。 对可疑 URL 单独跑,看到 num_redirects 异常增大或 curl 报 “Maximum redirects followed”,就是循环,需回到配置逐条排查。
步骤 7:上线后监控。 在 GSC 的”网页”报告中观察 404 数量是否上升,用” Crawl Stats”报告看服务器响应码分布;同时保留映射表至少一年,因为外链指向的旧地址可能在很久之后才被访问。迁移完成后的索引情况,可配合 Google Search Console 使用教程 持续跟踪。
常见踩坑
- 案例: 某站在 WordPress 后台改了固定链接结构后,用 Redirection 插件自动记录了 404 并逐条补跳转,但规则写成”包含匹配”,导致
/blog/a也匹配到/blog/abc的规则,两千多个 URL 跳到了错误页面。改成精确匹配并逐条验证后才修正。 - 用
rewrite ... permanent时带上了$args,把查询参数一起拼进新地址,产生带怪参数的重复 URL。需要保留参数时显式写$is_args$args,不需要时在末尾加?。 map放在server块内不生效(必须在http块),上线后发现全部跳转失效。- 只跳首页:把匹配不到的旧 URL 全部 301 到首页,Google 会判定为”不相关的重定向”,按 404 处理,权重照样丢失。无法映射的应老实返回 404。
- 跳转链:HTTP→HTTPS、www→非 www、旧路径→新路径三段分开配置,累积成三跳。应在一条规则里一步到位,可参考 301 302 307 重定向区别。
- 改完配置没有
nginx -t校验语法,也没有systemctl reload,配置根本没生效却以为已完成。 - 忽略大小写与结尾斜杠:
/About与/about/是不同 URL,映射表要覆盖两种形态,否则产生成片 404。
总结
批量重定向的质量标准可以量化:映射覆盖率(有外链的旧 URL 100% 覆盖)、单跳率(≥ 99% 的旧 URL 一次跳到位)、循环数(0)、上线后 30 天内 GSC 新增 404 数(趋近于 0)。行动清单:先合并 GSC、GA4、数据库三个来源建立旧 URL 全集;能用正则就用正则,不能就上 map;上线前用脚本逐条验证状态码与跳转次数;保留映射表一年以上。改版不可怕,可怕的是没有映射表就上线。
