Canonical URL 指错页:ZH 翻译指回 EN 原文——ZH 被 Google 去索引

ZH 页的 canonical 指向 EN 版——Google 把 ZH 去索引。每页 canonical 自指(current URL)、curl + view-source 验证。

打开 Search Console——ZH 页都显示”Alternate page with proper canonical tag”,意思是 Google 决定不索引——因为它们的 canonical 指别处。view-source 看 ZH 文章——<link rel="canonical" href="https://site.com/en/articles/foo/">——ZH 页把自己 canonical 到 EN 版。对 Google 而言 ZH 和 EN 是同一 URL——ZH 版从索引消失——双语投入的一半隐形。

通常来源:SEO 插件设成”用主语言版本”,或 layout 一直发 EN URL。修法概念上简单:每页 canonical 必须是自己的 URL。执行需要细心改模板、加 build 时检查、抽样 view-source 验证一次。

常见原因

1. SEO 插件配成把所有翻译 canonical 到主语言

有些插件(和一些自定义 layout)有”在主语言上集中权威”选项——听起来合理,实际错。hreflang 已经处理 locale 关系——canonical 应自指。

如何判断:view-source 一篇翻译页、查 canonical href。

curl -s https://site.com/zh/articles/foo/ | grep 'rel="canonical"'

href 指 /en/——插件配错。

2. Layout 里写死 canonical

有人在 article layout 里写 <link rel="canonical" href={https://site.com/en/articles/$\{slug\}/`} />`——EN 页凑合、ZH 页悄无声息坏。

如何判断:grep layout 中的 canonical。

grep -rn 'rel="canonical"' src/layouts/ src/components/

URL 不含 Astro.url 或当前 locale——就是写死。

3. Canonical URL 末尾斜杠不一致

服务器发 /en/articles/foo/ 但 canonical 声明 /en/articles/foo(无 /)——Google 看到两个 URL、按 canonical 那个索引——你真实的 URL 被降权。

如何判断:比 canonical href 和实际页 URL——末尾 / 必须匹配。

4. Canonical 含查询串或片段

作者分享了带 ?utm_source=twitter 的 URL——那串被缓存或写死进模板。canonical 现在含查询串——索引碎片。

如何判断:canonical href 含 ?#

5. 跨域 canonical 指向再发布版

把文章联合发到 Medium/Substack。有人把 Medium canonical 指你站(对)但你站的 canonical 指 Medium URL(错)——你自家的页告诉 Google”我是 Medium 的副本”。

如何判断:任何 canonical 主机名和页面主机名不匹配。

6. Canonical 完全缺失

完全没 canonical tag。Google 自己挑——通常 OK,但 URL 变体(带/不带 / 、带 utm)会让索引不确定。

如何判断:view-source 找 rel="canonical"——空——缺失。

最短修复路径

Step 1:canonical 每页自指

article layout 中从当前 URL 算 canonical:

---
const { article } = Astro.props;
const SITE = "https://site.com";
const canonical = `${SITE}/${article.data.lang}/articles/${article.data.urlSlug}/`;
---
<link rel="canonical" href={canonical} />

保证 ZH canonical 到 ZH、EN 到 EN。hreflang 单独告诉 Google 页面互为 alternate。

Step 2:锁定末尾斜杠和大小写

astro.config.mjs

export default defineConfig({
  trailingSlash: "always",
  build: { format: "directory" },
  site: "https://site.com",
});

Layout 中的 canonical URL 必须和服务器实发匹配。能用 Astro.site 就用(按 config 走末尾 /)。

Step 3:curl + view-source 抽样验证

每语言抽一篇、每大类一篇:

for url in \
  https://site.com/en/articles/foo/ \
  https://site.com/zh/articles/foo/ \
  https://site.com/en/articles/bar/ \
  https://site.com/zh/articles/bar/
do
  echo "=== $url ==="
  curl -s "$url" | grep -E 'rel="(canonical|alternate)"'
done

每页 canonical 匹配自己 URL、每对 alternate 互相声明。

Step 4:加 prebuild 断言

抓回退:

# scripts/audit-canonical.mjs
import fs from "node:fs";
import path from "node:path";

const distRoot = "dist";
let problems = 0;

function walk(dir) {
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, e.name);
    if (e.isDirectory()) { walk(p); continue; }
    if (!p.endsWith("index.html")) continue;
    const html = fs.readFileSync(p, "utf8");
    const m = html.match(/<link\s+rel="canonical"\s+href="([^"]+)"/);
    if (!m) { console.error(`MISSING canonical: ${p}`); problems++; continue; }
    // 从路径重构期望 URL
    const rel = p.replace(/^dist/, "").replace(/index\.html$/, "");
    const expected = `https://site.com${rel}`;
    if (m[1] !== expected) {
      console.error(`WRONG canonical: ${p} -> ${m[1]} (expected ${expected})`);
      problems++;
    }
  }
}
walk(distRoot);
process.exit(problems > 0 ? 1 : 0);

接到 postbuild。

Step 5:给之前被去索引的页请求重新索引

Search Console 中——给卡在”Alternate page with proper canonical tag”的页在 canonical 修好后请求 indexing。修法只在 Google 下次抓取时生效。

预防

  • canonical 从当前页 URL 算、永不写死
  • 末尾 / 政策在 Astro config 锁——canonical 和实发 URL 精确匹配
  • postbuild 审计:每页 canonical 匹配自己 URL
  • SEO 插件(如有)配置成集中翻译
  • canonical 中无查询串或片段
  • 跨域联合发布——原站 canonical 自指;只让联合副本 canonical 回原
  • 大模板改动后抽样 view-source 检查

相关

标签: #内容运营 #站点质量 #站点审计 #排查 #Canonical