<link rel="canonical"> 是你告诉 Google “如果这页有多个版本,把权重和收录算在哪个 URL 上”。一旦写错,会出现两种致命后果:你的真正想收录的页根本进不了索引,或者搜出来的 URL 是个错的版本(参数版、test 域名、备用语言、甚至别人的网站)。
最难发现的是:网页打开一切正常,只有 Google 知道你在自残。下面三种是真实见过的写法。
常见原因
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 → 抓 canonical URL → 404 或 noindex → 整个组(原页 + canonical)都不进索引。
如何判断:Search Console → 网址检查 → 看”Google 选择的规范网址”是否返回 200。或在终端:
curl -sI "https://yourdomain.com/legacy/post" | head -1
# 期望:HTTP/2 200
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" />
如何判断:Search Console → “页面”报告里搜 “Duplicate, Google chose different canonical”——这条专门捕这类错误。
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 排得很差。
另一类冲突:sitemap 里写 URL A,但页面 A 的 canonical 指 URL B。Google 会优先信 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 抓 canonical 时跳一次 301 是可以接受的,跳两次就视为弱信号,可能选另一版做主版本。
最短修复路径
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% 的页面应该指自己。在模板里强制:
<!-- src/layouts/Article.astro 之类的位置 -->
---
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:CI 加一层 build 阶段校验
在 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:修完后强制重抓
在 Search Console 对 5-10 个最重要的 URL 用”请求编入索引”。完整重新评估通常 1-4 周。
预防建议
- 模板里只通过一个
buildCanonical()helper 渲染 canonical,杜绝手写 - 新模板上线前用
audit-canonicals.mjs全站扫一次 - CI prebuild 拦截:缺失 canonical、noindex + canonical 共存、跨域 canonical 都报错
- canonical / sitemap / 内链三处大小写和斜杠保持完全一致,URL 统一走一个
urlFor()函数