最快的修法: 打开 Google Search Console,对排不上去的那一页跑 URL Inspection(网址检查),把 User-declared canonical(用户声明的规范网址)和 Google-selected canonical(Google 选定的规范网址)对比一下。两者不一致,就说明你的 canonical 标签指向了一个 Google 不愿意跟随的地方。大约 90% 的页面,正确值就是它自己的 URL(self-canonical)——设成自己、重新部署、再请求重新抓取即可。
<link rel="canonical"> 是在告诉 Google:「如果这段内容存在于多个 URL 上,把收录和排名信号都算到这一个上。」但 Google 自家文档说得很直白:「指定规范网址只是一个提示(hint),不是规则(rule)」——Google 会把你的标签和重定向、HTTP vs HTTPS、是否在 sitemap 里、内链等信号一起权衡,再挑出它最信的那个规范网址。标签写错,你就会落入两种结果之一:
- 你真正想收录的那一页根本进不了索引,或者
- 排出来的 URL 是个错版本——带参数的 URL、staging 子域、备用语言版,甚至别人的网站。
陷阱在于:浏览器里一切看着都正常,只有 Google 知道你在自残。下面四种写法全都是真实见过的。
你属于哪一类?
| Search Console 里的症状 | 可能原因 | 跳到 |
|---|---|---|
| Page Indexing 报告里,你本来想收录的页被标成 “Alternate page with proper canonical tag” | canonical 指向了别处(常是 noindex 或一个死链目标) | 原因 1 |
| URL Inspection 里 Google-selected canonical 是另一个域名或主机 | 跨域 / www-vs-裸域 canonical | 原因 2 |
| 错误语言版排上去了;整组语言版被并到了一个 URL | canonical 和 hreflang 打架 | 原因 3 |
| ”Duplicate, Google chose different canonical”——选中的 URL 只在斜杠 / 大小写 / 协议上有差别 | 归一化(normalization)不一致 | 原因 4 |
常见原因
1. canonical 指向不存在 / 404 / 被 noindex 的 URL
典型写法:
<!-- 模板写死了路径,但目标 URL 后来被删了 -->
<link rel="canonical" href="https://yourdomain.com/legacy/post" />
<!-- 域名换了,但模板没更新 -->
<link rel="canonical" href="https://staging.yourdomain.com/article" />
<!-- canonical 指向自己,但这页是 noindex -->
<meta name="robots" content="noindex" />
<link rel="canonical" href="https://yourdomain.com/this-very-page" />
Google 读到 canonical,去抓那个 URL,拿回 404 或 noindex,于是整个组(原页加上 canonical 目标)都从索引里掉出去。noindex 加 self-canonical 这个组合尤其阴险:你在同一个响应里同时对 Google 说「收录这一页」和「永远别收录这一页」。Google 会听 noindex。
如何确认: Search Console -> URL Inspection -> 展开 Page indexing,读 Google-selected canonical 这一栏(就挨着 User-declared canonical)。它有三种可能取值:“Inspected URL”(Google 选了这页自己)、“Same as user-declared canonical”(Google 接受了你的标签),或者一个不同的 URL(Google 推翻了你)。一个 2026 年 6 月仍然成立的坑:这两个 canonical 字段只会从已收录的数据里填出来,不会从 Test Live URL(测试实时网址)里出现——实时测试无法告诉你 Google 最终会选哪个 canonical。如果它显示的是另一个 URL,去抓那个 URL 看状态码:
curl -sI "https://yourdomain.com/legacy/post" | head -1
# 期望:HTTP/2 200(而不是 404、301 跳转链或一个 noindex 页)
2. 跨域 canonical 却没有这个意图,或目标不互相引用
跨站 canonical(指向另一个域名或 CDN 主机)只有在你确实希望信号转移到那个 URL 时才能如你所愿——比如自己写的内容做了同步发布(syndication)。常见的翻车:
- 复制了别站的 HTML 模板,忘了改 canonical,它还指着原网站
- 你通过 CDN 子域(
cdn.example.com)供给内容,却没把 canonical 指回主域 www.和裸域都能解析,canonical 一半指www.、一半指裸域
<!-- 你的页面在 www.yourdomain.com -->
<!-- 错误:把自己的权重拆给了裸域 -->
<link rel="canonical" href="https://yourdomain.com/article" />
注意 Google 自家在规范化文档里的提醒:对于分发给同步合作方(syndication partners)的内容,canonical 链接元素不是推荐的工具,「因为这些页面往往差别很大」——Google 反而要求同步合作方阻止收录那份副本。只有当副本确实是你掌控下、完全相同的内容时,才用跨域 canonical。
如何确认: Search Console -> Page Indexing 报告 -> 打开 “Duplicate, Google chose different canonical”。这个状态专门对应这类情况——Google 觉得另一个主机更权威,把信号并过去了。
3. canonical 和 hreflang / robots / sitemap 互相打架
hreflang 要求每个语言版本都引用其余所有版本,但 canonical 必须指向它自己的语言版。否则两套信号互相抵消:
<!-- 在 /zh/article 页上 -->
<link rel="canonical" href="https://yourdomain.com/zh/article/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/article/" />
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/article/" />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/article/" />
如果中文页的 canonical 错指向英文页,整个中文语言组都可能从索引里掉出去——只剩英文页排名,而且对中文 query 排得很差。每个翻译 URL 都必须 self-canonical,再用 hreflang 互相引用同胞版本。
另一类冲突:你的 sitemap 列的是 URL A,但页面 A 的 canonical 指向 URL B。Google 把 rel="canonical" 当成比「在 sitemap 里」更强的信号,所以它会跟随 canonical,sitemap 里那条等于白提交。
4. canonical 在大小写 / trailing slash / 协议上不一致
下面这些在 Google 眼里都是不同的 URL:
HTTPS://yourdomain.com/Articlevshttps://yourdomain.com/articlehttps://yourdomain.com/articlevshttps://yourdomain.com/article/https://yourdomain.com/articlevshttp://yourdomain.com/article
如果 sitemap、内链、canonical 在大小写、斜杠或协议上各说各话,你就是在给 Google 喂互相矛盾的信号。Google 一般偏好 HTTPS 版本,也能消化掉一次干净的重定向跳转;但跳转链(比如 http -> https -> 带斜杠的变体)会把信号削弱到 Google 可能选一个你并不想要的版本当 canonical。
最短修复路径
Step 1:全站审计 canonical
下面这段脚本会遍历你的 sitemap,把每个 URL 的 canonical 抽出来,并标记上面四种翻车模式:
// scripts/audit-canonicals.mjs
import { XMLParser } from "fast-xml-parser";
const sitemapUrl = "https://yourdomain.com/sitemap.xml";
const expectedHost = "yourdomain.com";
const xml = await fetch(sitemapUrl).then((r) => r.text());
const { urlset } = new XMLParser().parse(xml);
const urls = urlset.url.map((u) => u.loc);
for (const url of urls) {
const html = await fetch(url).then((r) => r.text());
const m = html.match(/<link\s+rel=["']canonical["']\s+href=["']([^"']+)["']/i);
const canonical = m?.[1] ?? "(missing)";
const issues = [];
if (canonical === "(missing)") issues.push("MISSING");
else {
const c = new URL(canonical);
if (c.host !== expectedHost) issues.push(`CROSS-HOST: ${c.host}`);
if (c.protocol !== "https:") issues.push("NON-HTTPS");
if (c.pathname !== new URL(url).pathname) issues.push("PATH-DIFFERS");
}
console.log(`${url}\t-> ${canonical}\t${issues.join(",")}`);
}
运行 node scripts/audit-canonicals.mjs > canonicals.tsv,扫一遍被标记的行即可。
Step 2:默认全部 self-canonical,只在必要时指别处
大约 90% 的页面都应该指向自己。在你的布局里:
---
const canonical = Astro.url.href;
---
<link rel="canonical" href={canonical} />
只在以下这些具体情况下才指别处:
| 情况 | canonical 指向 |
|---|---|
分页 /blog?page=2 | /blog |
参数变体 /p?utm=x | /p |
移动子域 m.example.com/p | example.com/p |
| 自己同步发布(你写的,也发到了别处) | 你自己的主版本 |
| 转载别人的(别人原创) | 对方的主版本 |
Step 3:canonical 和 hreflang 同时出现时,用一个 helper 一起生成
export function buildHreflangAndCanonical(currentLang, slug, langs) {
const base = "https://yourdomain.com";
const canonical = `${base}/${currentLang}/${slug}/`;
const alternates = langs.map((l) => ({
hreflang: l,
href: `${base}/${l}/${slug}/`,
}));
alternates.push({ hreflang: "x-default", href: `${base}/en/${slug}/` });
return { canonical, alternates };
}
每个页面都过这个函数。canonical 永远是当前语言版自己;hreflang 覆盖所有语言再加 x-default;循环永远闭环。
Step 4:加一道构建期校验
在 prebuild 里:
// scripts/check-canonical-build.mjs
import fg from "fast-glob";
import fs from "node:fs";
const files = fg.sync("dist/**/*.html");
const issues = [];
for (const f of files) {
const html = fs.readFileSync(f, "utf8");
const cm = html.match(/<link\s+rel=["']canonical["']\s+href=["']([^"']+)["']/i);
const robots = html.match(/<meta\s+name=["']robots["']\s+content=["']([^"']+)["']/i);
if (!cm) issues.push(`${f}: MISSING canonical`);
if (robots?.[1]?.includes("noindex") && cm) {
issues.push(`${f}: noindex + canonical (canonical wasted)`);
}
}
if (issues.length) {
console.error(issues.join("\n"));
process.exit(1);
}
这能在部署前就抓住「模板写错了 canonical」。
Step 5:对关键 URL 强制重抓
在 Search Console 里,对你最重要的 5-10 个 URL 跑 URL Inspection -> Request Indexing(请求编入索引)。截至 2026 年 6 月,URL Inspection 工具对每个资源(property)每天大约只放 10-12 个手动请求(Google 未公布确切数字);一旦触顶,Request Indexing 按钮会变灰约 24 小时,而且这个上限是按已验证的资源算的,换账号或换浏览器都不会重置它。所以把请求花在最有价值的页面上。重新评估不是即时的:预计要几天到约两周,Google 才会重抓并重新处理修正后的 canonical。
如何确认已经修好
在 Google 认账之前,你什么都还没修好。重新部署后:
- URL Inspection -> Test Live URL -> 确认线上页面现在显示的 User-declared canonical 正确(self-canonical 的页面就是它自己的 URL)。
- 等重抓完成后,再检查已收录的版本:Google-selected canonical 应该读出 “Inspected URL” 或 “Same as user-declared canonical”。
- 在 Page Indexing 报告里,这页应该从 “Alternate page with proper canonical tag” / “Duplicate, Google chose different canonical” 移出,进入 Indexed。
- 用
curl抽查:
curl -s "https://yourdomain.com/article/" | grep -i 'rel="canonical"'
# 应该只打印出一个 canonical,并且指向这个 URL 自己。
常见问题
为什么 Google 无视我的 canonical 标签? 因为 canonical 是提示(hint),不是指令。如果你的其它信号和它矛盾——内链、sitemap、重定向,或者 HTTP/HTTPS 不一致——Google 的启发式判断就可能推翻这个标签,另选一个 canonical。把所有信号都对齐到同一个 URL 上,这种推翻通常就消失了。
“Alternate page with proper canonical tag” 算错误吗? 本身不算。对于你有意 canonical 指向别处的页面(参数 URL、分页),这正是正确的状态。只有当它出现在你本来想收录的页面上时才是问题——那说明 canonical 指错了地方。
同一页上能同时放 canonical 和 noindex 吗?
不能——它们互相矛盾,而 Google 会听 noindex。二选一:要么收录这页(self-canonical,去掉 noindex),要么把它移出索引(noindex,并删掉那个指向自己的 canonical)。
是不是每页都该 self-canonical? 大约 90% 都该。只有对你掌控下的真正重复内容才指别处:分页、跟踪参数变体、移动子域、自己同步发布的副本。拿不准时,就 self-canonical。
修完后多久能在搜索里体现? 请求编入索引之后,预计几天到约两周,Google 才会重抓并重新处理。只有当已收录版本的 Google-selected canonical 反映了修改,这件事才算「完成」——而不是你部署的那一刻。
canonical 能像 301 一样传递全部排名权重吗?
很接近,但 301 重定向是更强的合并信号。当旧 URL 确实该消失时用 301;当两个 URL 都必须保持可访问(比如打印版或参数版)时用 rel="canonical"。
预防建议
- 所有 canonical 都走同一个
buildCanonical()helper——杜绝手写标签。 - 新模板上线前,用
audit-canonicals.mjs全站扫一遍。 - CI prebuild 拦截:缺失 canonical、
noindex和 canonical 共存、跨域 canonical。 - canonical / sitemap / 内链在大小写和 trailing slash 上完全一致——每个 URL 都通过同一个
urlFor()函数生成。
相关阅读
- 换域名后 canonical 错了
- Alternate page with proper canonical tag
- Duplicate, Google chose different canonical
- Googlebot 抓取上升但曝光不动
- 无限滚动页内容未被收录:3 个原因 + 修复路径
- “Page not mobile friendly” 怎么修
外部参考:Google: URL canonicalization、Google: fix canonicalization issues