主域名从 oldsite.com 切到 newsite.com,DNS 也指过去了,新域名能正常访问,但用 view-source 打开线上页面,里面的 <link rel="canonical" href="https://oldsite.com/..."> 还是老域名。放着不管,Google 会继续把老域名当作主体(primary),搜索流量始终回不到新域名。
最快的修法: canonical 几乎总是在 build 时从一个写死的站点 URL 嵌进去、再被 CDN 缓存冻住的。在仓库里 grep 老域名,全部换成一个环境变量(SITE_URL),重新 build、重新部署、清掉 CDN 缓存,再用 curl 验证。Search Console 的”地址变更”(Change of address)那一步是用来告诉 Google “这是一次永久迁移”的——一定要做,但它不会改写你的 HTML。
这种问题很少只有一个根因,通常是一叠:代码里硬编码 URL + CDN 缓存 + sitemap/301 没更新 + Google 还没重爬。按顺序一层层剥开。
你属于哪一类?
| 你能直接观察到的现象 | 最可能的那一层 | 跳到 |
|---|---|---|
curl -s https://newsite.com/page | grep canonical 在全新部署后仍显示 oldsite | 源码里写死了站点 URL | Step 1 |
dist/ 里源码已修好,但生产仍 serve 老 canonical;curl -I 显示缓存 HIT | CDN / 宿主缓存 | Step 3 |
sitemap.xml 的 <loc> 还列着 oldsite.com | sitemap / robots 没更新 | Step 4 |
oldsite.com 301 到新域名,但新页面 canonical 又指回老的 | 301 ↔ canonical 死循环 | Step 4 |
| HTML 处处都对了,GSC 仍把老域名当 canonical | Google 还没重新处理 | Step 5 |
在 Search Console 的 网页(Pages) 报告(以前叫”索引涵盖范围”)里,要重点看两个状态:Duplicate, Google chose different canonical than user(严重——Google 在覆盖你声明的 canonical,通常是因为老 URL 仍有更多外链 / sitemap 权重),以及 Duplicate without user-selected canonical。而 Alternate page with proper canonical tag 只是信息性提示,一般没问题——它表示 Google 看到了你的 canonical 并照做了。
常见原因(按命中率排序)
1. 站点根 URL 在源码里写死
astro.config.mjs / next.config.js / 模板里直接写了字面量 https://oldsite.com,build 时把这个值嵌进每个页面的 canonical、og:url 和 sitemap.xml。哪怕宿主域名换了,HTML 里发出去的还是老域名。
典型代码:
// astro.config.mjs
export default defineConfig({
site: 'https://oldsite.com', // ← 一直没改
});
如何判断:grep -r "oldsite.com" src/ astro.config.mjs next.config.js public/,数一下还有几处残留。
2. CDN / 宿主在 serve 缓存的 HTML
代码改完也重新部署了,但 Cloudflare / Vercel Edge / Fastly 还在 serve 上一版本的 HTML。curl -I 看到 cf-cache-status: HIT 或 x-vercel-cache: HIT——你看到的是缓存,不是新 build。
如何判断:curl -s https://newsite.com/some-page | grep canonical 仍是 oldsite,而 curl -I 显示缓存 HIT。
3. sitemap 和 robots.txt 没同步更新
sitemap.xml 还在列 https://oldsite.com/...。一旦提交,Google 会继续抓老 URL,而 sitemap 里这些非 canonical 的 URL 会主动把 Google 往老域名上推。robots.txt 里如果写了指向老域名的绝对路径 sitemap,效果一样糟。
如何判断:curl https://newsite.com/sitemap.xml | head -50,看 <loc> 字段是不是新域名。
4. 301 重定向只配了一半
oldsite.com 全量 301 到 newsite.com 做好了,但新域名上的页面 canonical 又指回 oldsite.com。Google 就看到 301 → canonical → 301 绕成一圈,干脆两个 URL 都不收。
如何判断:curl -I https://oldsite.com/page 看 301 目标,再 curl https://newsite.com/page | grep canonical 看 canonical。如果两者互相指,就是死循环。
5. Search Console 的”地址变更”没提交
地址变更(Change of address)工具是用来在整站层面告诉 Google “这次迁移是永久的”——单靠 301 只能传递逐 URL 的信号。截至 2026 年 6 月有两个坑:该工具只能在两个 网域资源(Domain property) 之间用(不能用 URL-prefix 前缀资源),而且你必须在同一个 Google 账号下同时是新旧两个资源的”已验证所有者”(Verified Owner)。
如何判断:在 Search Console 打开老资源 → 设置 → 地址变更。如果它是灰的或干脆没有,多半是你的资源是 URL-prefix 类型,或者你还没拥有新资源。
最短修复路径
Step 1:grep 老域名并改为单一 env var
grep -rIn "oldsite\.com" . \
--exclude-dir=node_modules \
--exclude-dir=.git \
--exclude-dir=dist
把每一处硬编码都换成读 env var:
// astro.config.mjs
export default defineConfig({
site: process.env.SITE_URL || 'https://newsite.com',
});
// src/lib/seo.ts
export const SITE_URL = import.meta.env.SITE_URL || 'https://newsite.com';
// 模板里
<link rel="canonical" href={`${SITE_URL}${Astro.url.pathname}`} />
每个新域名页面都应当带**自引用(self-referencing)**的 canonical——也就是页面指向它自己的新域名 URL。Google 的整站迁移指南明确要求:一旦 301 生效,新页面的 rel="canonical" 必须使用新 URL。迁移期间页面 canonical 指向任何别的 URL,是把迁移搞乱的最快方式。
在宿主(Vercel / Netlify / Cloudflare)上把 SITE_URL=https://newsite.com 配好,preview 和 production 两个环境都要设。两个环境取值不一致,是”本地好使、上了生产就不行”的常见原因。
Step 2:重新 build + 部署 + 验证产物
# 本地先验
npm run build
grep -rIn "oldsite.com" dist/ || echo "clean"
# 部署到生产后再验
curl -s https://newsite.com/ | grep -E '<link rel="canonical"|og:url'
每一处匹配都应该是 https://newsite.com/...。如果 dist/ 干净但生产仍显示老域名,那就是命中缓存了——去 Step 3。
Step 3:清 CDN / 宿主缓存
- Cloudflare:dashboard → Caching → Configuration → Purge Everything(在警告弹窗里确认)。
- Vercel:Deployments → 最新 deploy → Redeploy,不勾选 “Use existing Build Cache”。命令行用
vercel --force,或设VERCEL_FORCE_NO_BUILD_CACHE=1。Vercel 的 edge cache 是按每次 deploy 分键的,所以一次全新 deploy 会 serve 全新 HTML。 - Cloudflare Pages:Settings → Build → 清掉 build cache,再重新部署。
- Netlify:Deploys → Trigger deploy → Clear cache and deploy site。
清完之后,下一次(非首次)请求 curl -I https://newsite.com/some-page 应显示 cf-cache-status: MISS(或 EXPIRED),并且 curl 应返回新的 canonical。
Step 4:更新 sitemap、robots、301 重定向
public/robots.txt:
User-agent: *
Allow: /
Sitemap: https://newsite.com/sitemap.xml
在老域名上配一条全路径 301 到新域名(三选一:Cloudflare Page Rule / Vercel vercel.json / Nginx):
// vercel.json(配在 oldsite 项目上)
{
"redirects": [
{ "source": "/(.*)", "destination": "https://newsite.com/$1", "permanent": true }
]
}
验证重定向保留了路径(只把首页 301 过去是常见错误,会丢掉所有深层链接):
curl -I https://oldsite.com/some-page
# HTTP/2 301
# location: https://newsite.com/some-page
Step 5:在 Search Console 提交”地址变更”
- 在 GSC 把
https://newsite.com添加为资源并验证。新旧两边都用 网域资源(Domain property),这样”地址变更”工具才可用。 - 先把首页 301 实现到位——该工具会跑迁移前检查(pre-move check),如果首页缺少
oldsite.com -> newsite.com的 301,会被标成”严重失败”(critical failure)。 - 在老资源里,进入 设置 → 地址变更,选中新资源并提交。所有严重(critical)检查必须先修好才能继续;非严重检查只是警告,可以通过。
- 在新资源下提交新 sitemap(
https://newsite.com/sitemap.xml)。 - 对最重要的几个页面,用 URL 检查(URL Inspection)→ “请求编入索引”(Request indexing)来催一次重爬。截至 2026 年 6 月,手动配额很小(大约每个资源每天 10–12 个 URL,且是滚动 24 小时窗口),所以挑少数几个核心页,其余交给 sitemap 带。
对一个中小站点,Google 通常要几周才能把大部分页面迁过去,大站更久,重爬期间排名波动属正常。整个过程里老域名的 301 必须一直在线——截至 2026 年 6 月,Google 的整站迁移指南说至少 180 天,并且”尽可能久,一般至少 1 年”;如果你还能看到老 URL 上有 Search 流量,就再留更久。
如何确认已修好
curl -s https://newsite.com/ | grep canonical只返回https://newsite.com/...——任何地方都不再有 oldsite。curl -I https://newsite.com/显示是新鲜响应(cf-cache-status: MISS/EXPIRED,或一个新的x-vercel-id)。curl -I https://oldsite.com/deep/path返回301,指向新域名上相同的深层路径。- 在 GSC 对一个新域名 URL 跑 URL 检查 → “Google 选择的规范网址”(Google-selected canonical)与你声明的 canonical 一致。
- 接下来几周,老域名在 GSC 的展示次数(impressions)趋势下降,新域名趋势上升。
常见问题
Google 多久才不再显示老域名? 对中小站点,大部分页面迁移要几周,完全稳定下来要到几个月,即便所有配置都对了也一样;大站更久。地址变更信号和 301 能加速,但不会让它瞬间完成。301 至少保留 180 天——Google 现在的措辞是”一般至少 1 年”。
HTML 都对了,但 GSC 还说 canonical 是老域名,为什么?
Google 缓存的是它自己的抓取结果。如果当前 HTML、sitemap、301 三者一致,下一次重爬就会修正。如果一直不变,检查 Google 选的那个 URL 是不是被一条过期的 sitemap 条目、或更多指向老 URL 的外链喂着——那就是 Duplicate, Google chose different canonical than user 的情况。
有了 301,还需要地址变更工具吗? 301 传递的是逐 URL 信号;地址变更再加一个整站层面”这是永久迁移”的信号,加速合并。两个都用。注意:它不适用于 HTTP→HTTPS 的迁移(那种情况按标准的整站迁移指南做),而且它要求新旧两边都是你拥有的网域资源。
我清了缓存,但还是看到老 canonical。
先确认 build 产物本身是干净的(grep -rIn "oldsite.com" dist/)。如果 dist/ 干净、curl -I 显示 MISS 但响应体仍是旧的,可能有第二层缓存(比如 Cloudflare 套在 Vercel 前面)——两层都清,并检查浏览器里有没有 service worker 在缓存页面。
老域名要永久保留吗? 不必,但它和它的 301 至少留 180 天,最好留满一年(Google 自己的措辞是”尽可能久,一般至少 1 年”),让 Google 在你让域名过期前充分合并排名信号。301 不会让你损失 PageRank,所以留久一点没有坏处。
预防建议
- 站点根 URL 只从一个 env var 读;禁止任何文件里出现字面量域名字符串。
- 加一条 CI 规则:
grep -r "oldsite.com" src/命中就 fail build。 - 每次大改动(域名、路由结构、canonical 模板)之后,对约 10 个有代表性的 URL 跑一遍 canonical 冒烟测试。
- 用
@astrojs/sitemap(或同类插件)按site自动生成sitemap.xml——绝不要手写。 - 老域名的 301 至少保留 180 天(按 Google 现行指南,留满一年更稳)以给 Google 充足的合并时间。
相关阅读
外部参考:Google:地址变更工具 和 Google:含 URL 变更的整站迁移。