打开 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 检查
相关
- Content Site Hreflang Tags Misconfigured
- Bilingual Pages Drift Apart Over Time
- Content Site Translation Pages Mismatched
- Content Site Sitemap Not Resubmitted After Big Changes
- Search Console Low Value URLs
- Content Site FAQ Schema Not Extracted
标签: #内容运营 #站点质量 #站点审计 #排查 #Canonical