你在 vercel.json / _redirects / netlify.toml 里写了一条 /old-path → /new-path 的 301,部署完去浏览器里访问,结果 404 或者继续显示旧页面——这是 web 部署里最常见的”配置看起来对但就是没跑”问题。多数情况是因为规则文件没被 build 收录、规则顺序被前面的 catch-all 拦截、或者 CDN / 浏览器还在缓存旧响应。
本文按命中率拆开 5 类原因,并给出可以直接 curl -I 验证的修复路径。
常见原因
按命中率从高到低:
1. 规则文件不在 build 产物里
vercel.json 必须在仓库根目录;Netlify 的 _redirects 必须在 public/ 里(Astro / Vite 系),构建后会被复制到 dist/_redirects;Cloudflare Pages 同样要求 _redirects 出现在最终发布目录里。如果你把文件放在了 src/ 或者 app/,构建器不会自动复制过去。
如何判断:本地跑 npm run build 后看 ls dist/ 或 ls .vercel/output/,确认 _redirects 或 config.json 真的在那。Vercel 还可以在 Deployments → 选一次部署 → “Source” 标签里直接搜文件。
2. 顺序问题——前面的 catch-all 抢先匹配
重定向引擎大多按”自上而下,第一个匹配胜出”。如果你在 vercel.json 里先写了:
{
"redirects": [
{ "source": "/:path*", "destination": "/new-site/:path*", "permanent": true },
{ "source": "/old", "destination": "/new", "permanent": true }
]
}
第二条永远跑不到,因为第一条已经吞掉所有路径。
如何判断:把 /old 单独提到列表第一条试一次,如果立刻 work,就是顺序问题。
3. CDN / 浏览器缓存旧响应
如果上一次部署没设置重定向、且 CDN 给那条 URL 缓存了 200 + HTML,新规则不会立刻生效。Cloudflare 默认对 HTML 有 4 小时 edge cache,Vercel Edge 也会缓存 redirect 响应本身。
如何判断:curl -I "https://yourdomain.com/old-path?cb=$(date +%s)" 加 cache buster 看响应头。如果带 buster 是 301 但不带是 200,就是缓存问题。
4. trailing slash 不匹配
很多平台对 /old 和 /old/ 视为两条不同 URL。Vercel 默认 trailingSlash: false,规则写 /old/ 永远不会匹配用户访问的 /old。Netlify _redirects 同理。
如何判断:把规则源里的斜杠和你站点实际 URL 风格对齐再测一次。
5. 用了 rewrites 但期望的是 redirects
rewrites 是 URL 不变、内部代理到另一个路径,用户地址栏看到的还是 /old。redirects 才会让浏览器跳到 /new 并返回 301 / 302。两个字段写错对方就会得到”看起来跳了又没跳”的怪现象。
如何判断:curl -I 看返回码——200 是 rewrite,301/302/308 才是 redirect。
最短修复路径
按收益从高到低,前 3 步通常就能解决 80% 的问题。
Step 1:用 curl 看真实响应
不要只看浏览器地址栏,浏览器会自动跟随跳转、还可能命中本地缓存。直接 curl:
curl -I -L "https://yourdomain.com/old-path?cb=$(date +%s)"
读输出:
- 第一个响应是
301 Moved Permanently+location: /new-path→ 重定向生效 - 第一个响应是
200→ 规则没跑(看 Step 2) - 第一个响应是
308且 location 指向自己加了斜杠 → 是平台的 trailing slash 行为,不是你的规则 cf-cache-status: HIT/x-vercel-cache: HIT→ 缓存问题(看 Step 3)
Step 2:确认规则文件在产物里、顺序正确
本地构建并检查:
npm run build
ls -la dist/ | grep -E '_redirects|vercel.json'
cat dist/_redirects 2>/dev/null || cat vercel.json
如果文件不在 dist/,按平台规范放正确位置:
| 平台 | 规则文件 | 必须放在 |
|---|---|---|
| Vercel | vercel.json | 仓库根目录 |
| Netlify | _redirects 或 netlify.toml | public/ 或根目录 |
| Cloudflare Pages | _redirects | 发布目录(通常 public/ 或 dist/) |
| Astro 静态 | _redirects | public/_redirects |
然后把具体规则放到通用规则之前。Vercel 示例:
{
"redirects": [
{ "source": "/old", "destination": "/new", "permanent": true },
{ "source": "/blog/:slug", "destination": "/articles/:slug", "permanent": true },
{ "source": "/legacy/:path*", "destination": "/", "permanent": false }
]
}
Step 3:清 CDN,针对单个 URL
不要一上来就 purge everything——成本高、命中率低。针对那条具体 URL 清:
- Cloudflare:Caching → Configuration → Purge Custom URLs,粘贴完整 URL(含
https://) - Vercel:重新部署,或对该项目跑
vercel --prod --force - Netlify:Deploys → 选最新部署 → Trigger deploy → Clear cache and deploy site
清完再用 Step 1 的 curl + cache buster 验证。
Step 4:加 CI 冒烟测试
把重定向写完后,最容易的回归保护是在部署 hook 后跑几条 curl,断言响应码和 location。例:
#!/usr/bin/env bash
set -e
BASE="https://yourdomain.com"
check() {
local from="$1" expected="$2"
local actual=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "$BASE$from")
if [[ "$actual" != "$expected"* ]]; then
echo "FAIL $from → got '$actual', expected '$expected'"; exit 1
fi
}
check "/old" "301 ${BASE}/new"
check "/blog/hello" "301 ${BASE}/articles/hello"
echo "All redirects OK"
在 GitHub Actions 的 deployment_status 事件里跑这段即可。
预防建议
- 写完重定向立刻
curl -I验证,不要靠浏览器看 - 规则放序:具体在前、catch-all 在后;用注释标好
- 部署后跑 5-10 条关键 URL 的冒烟测试,挂 CI
- 统一站点的 trailing slash 策略,规则源里和实际一致
- 改大批量重定向前先 git commit,便于按文件 diff 回滚