Hreflang 配置错乱:Search Console 红字告警——translationKey 自动发 + 第三方校验

hreflang 不互引、语言代码不一致(zh vs zh-CN)、缺 x-default。从 translationKey 自动发、用 hreflang.org 校验、源头修一次。

Search Console -> International Targeting -> Hreflang 一片红。“No return tags” 80 对、“Invalid language code” 12 个、零星”Unrecognized hreflang values”。你建的双语站从底层接线就坏——页面互相声明 alternate 但不对称、语言代码不是 Google 期望的、有一端缺 x-default。结果:Google 无法可靠把正确语言版换进 SERP——用户被发错语言。

Hreflang 不留情:对中两页必须互相声明、必须同语言代码、URL 必须精确匹配(末尾 / 算数)、x-default 必须指合理 fallback。修法:从单一真相源(translationKey)自动发、用第三方 hreflang 校验器验、永远别再手写 hreflang。

常见原因

1. 一边声明对、另一边没

EN 页声明 ZH 为 alternate。ZH 页声明 EN 为 alternate(或声明了不同 EN URL)。Google 要求互相声明——Search Console 报”No return tags”。

如何判断:取两页、grep link rel="alternate"、确认互相列。

curl -s https://site.com/en/articles/foo/ | grep 'hreflang'
curl -s https://site.com/zh/articles/foo/ | grep 'hreflang'

2. 语言代码不一致(zh vs zh-CN vs zh-Hans)

EN 页写 hreflang="zh";ZH 页写 hreflang="zh-CN"。Google 看到自指不一致——页面互相声明但代码不一致。要么全用 zh、要么全用 zh-CN

如何判断:grep 模板里的 hreflang 代码。

grep -rn 'hreflang=' src/layouts/ src/components/

不同地方不同代码——就是这病。

3. 末尾斜杠不一致

EN 把 ZH alternate 写成 https://site.com/zh/articles/foo,实际 ZH URL 是 https://site.com/zh/articles/foo/(带 /)。Google 看它们是不同 URL——声明算坏。

如何判断:sitemap 的 canonical URL 和 hreflang URL 必须字节级匹配。

4. 缺 x-default

声明了 enzh alternate 但没声明 x-default。没有它——Google 给两边都不匹配国家的用户瞎猜。最佳实践:x-default 指 EN(或主语言)。

如何判断:grep 模板输出的 x-default。

curl -s https://site.com/en/articles/foo/ | grep 'x-default'

空——缺 x-default。

5. 在无翻译的页面发 hreflang

你给每篇 EN 都发 zh alternate——即便没 ZH 对应。“alternate”指向 404 或根——Google 记为坏。

如何判断:页面声明的 alternate URL 无法解析。

6. hreflang 只在 sitemap 或只在 HTML head——或两处不一致

可以用 sitemap 或 HTML head——两处都做且不一致是最坏。选一个、坚持。

如何判断:比 sitemap 和页面 HTML 中的 hreflang——任何不一致都坏。

最短修复路径

Step 1:从 translationKey 自动发 hreflang

单一真相源。在 article layout 中按 translationKey 查兄弟——为每个真有对应的 locale 发一条 alternate:

---
import { getCollection } from "astro:content";
const { article } = Astro.props;
const all = await getCollection("articles");
const siblings = all.filter(a => a.data.translationKey === article.data.translationKey);
const SITE = "https://site.com";
---
{siblings.map(s => (
  <link
    rel="alternate"
    hreflang={s.data.lang === "zh" ? "zh-CN" : s.data.lang}
    href={`${SITE}/${s.data.lang}/articles/${s.data.urlSlug}/`}
  />
))}
<link rel="alternate" hreflang="x-default" href={`${SITE}/en/articles/${article.data.urlSlug}/`} />

保证互引。兄弟不存在——那个 locale 不发 alternate。

Step 2:选定语言代码约定并统一

两个合理选择:

- "zh"(仅语言)——简单,只有一种 ZH 变体够用
- "zh-CN"(语言+地区)——显式,未来可能加 zh-TW 时推荐

选一个、改模板、grep 验证没其他代码出现。

Step 3:规范 URL(末尾斜杠)

Astro 默认 content collection URL 带末尾 /。在 astro.config.mjs 锁定:

export default defineConfig({
  trailingSlash: "always",
  build: { format: "directory" },
});

并确保 hreflang URL 和页面 canonical URL 精确匹配。

Step 4:用第三方工具校验

部署后跑外部校验器:

- https://hreflang.org/ —— 贴 URL、看对和错
- https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/ —— 生成 + 检查
- Search Console -> International Targeting -> Hreflang errors 报告

修标记的问题,重新校验。

Step 5:加 prebuild 断言

部署前抓回退:

# scripts/audit-hreflang-pairs.mjs
import { getCollection } from "astro:content";
const all = await getCollection("articles");
const byKey = new Map();
for (const a of all) {
  if (!a.data.translationKey) continue;
  if (!byKey.has(a.data.translationKey)) byKey.set(a.data.translationKey, []);
  byKey.get(a.data.translationKey).push(a);
}
let problems = 0;
for (const [key, group] of byKey) {
  const langs = new Set(group.map(a => a.data.lang));
  if (langs.size === 1) {
    // 单语——hreflang 模板必须不发 alternate(合理,非问题)
  }
}
console.log(`Hreflang audit: ${problems} problems`);
process.exit(problems > 0 ? 1 : 0);

Step 6:重抓 + 重提受影响 URL

在 Search Console 给修过的一批页请求 indexing——Google 重抓看到正确 tags。hreflang errors 报告以天为单位更新——别等几分钟。

预防

  • hreflang 从 translationKey 生成,永不手写
  • 站点统一一种语言代码约定(如 zh-CN)—— lint 强制
  • 末尾斜杠政策在 Astro config 锁——hreflang URL 和 canonical URL 匹配
  • x-default 始终存在,指主语言
  • prebuild 断言:每对互相声明、代码匹配
  • 大部署后跑外部校验器(hreflang.org 或类似)
  • 每月看一次 Search Console 的 hreflang 错误报告

相关

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