hreflang 警告快速指南:return tag 缺失和语言码错

hreflang 警告到底什么意思、为什么会出现配对不闭环、return tag 和语言码错的最短修复路径。要看 Search Console International Targeting 报告里的错误,看相关页。

hreflang 是告诉 Google “这个页面在另一个语言/区域有等价版本,搜对应语言用户时请用那一版”。它的规则严格到几乎无法靠手写正确:每个语言版本必须互相引用(return tag),语言码必须是 ISO 639-1,地区码必须是 ISO 3166-1,加上一个 x-default——任意一处漏写或错写,整组 hreflang 就失效。

下面是 Search Console “International Targeting” / “Pages” 报告里 hreflang 类警告最常见的几种和最短修复路径。

常见原因

1. Return tag 缺失(最高频)

例子:

<!-- /zh/article 页面 -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/article/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/article/" />

<!-- /en/article 页面(错:忘记了 zh return tag) -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/article/" />
<!-- 这里漏了 <link rel="alternate" hreflang="zh" href="...zh..."> -->

只要英文页没回引中文页,整对就破。Search Console 报 “No return tags”。

如何判断:在 Search Console → International Targeting → 看是否有 “Errors: No return tags”;或者用 hreflang.org 在线检查器 输入两个 URL 互查。

2. 语言码 / 区域码写错

错写法正确写法说明
zhzhzh-Hans / zh-Hantzh 自身是合法的,但简繁中文都用 zh 会被覆盖
cnzh-CNcn 是地区不是语言
twzh-TW同上
zh-hkzh-HK大小写错误(Google 容忍,但有些工具不)
en-usen-US同上
en-uken-GBUK 不是 ISO 国家码,GB 才是
zh-CN-Hanszh-Hans-CN脚本码在前,地区码在后
jpja日语是 jajp 是国家码

如何判断:抓任意一个页面的所有 <link rel="alternate" hreflang="...">,把 hreflang 值列出来,逐一对照上表。

3. 缺 x-default

x-default 告诉 Google “如果用户的语言不在你列出的版本里,应该跳到哪一个”。不写 → Google 自己猜,可能把英文用户送去日语版。

<link rel="alternate" hreflang="en" href="..." />
<link rel="alternate" hreflang="zh" href="..." />
<link rel="alternate" hreflang="x-default" href="https://yourdomain.com/en/article/" />

通常 x-default 指向主语言版本(往往是英文)。

4. hreflang URL 指向了 404 / 重定向 / noindex 页

<!-- /zh/post -->
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/post/" />
<!-- 但 /en/post/ 已经被删了,或被重定向,或 noindex -->

Google 会丢弃整对。

如何判断

for u in /en/post /zh/post /ja/post; do
  echo -n "$u: "
  curl -sI -o /dev/null -w "%{http_code}\n" "https://yourdomain.com$u/"
done
# 期望全部 200,且 head 里没有 noindex

5. 同语言两个 URL 互相宣称是 hreflang

<!-- /zh/post 页面 -->
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/post-v2/" />
<!-- 同一语言又指了第二个 URL -->

每个 hreflang 值(如 zh)只能出现一次。多个 → Google 全部忽略。

6. hreflang 与 canonical 冲突(自杀级)

最致命的写法:

<!-- /zh/post 页面 -->
<link rel="canonical" href="https://yourdomain.com/en/post/" />
<link rel="alternate" hreflang="zh" href="https://yourdomain.com/zh/post/" />
<link rel="alternate" hreflang="en" href="https://yourdomain.com/en/post/" />

canonical 指向英文版意味着”中文版不要收录”,hreflang 又试图让中文版作为 zh 流量目的地——Google 直接把中文版踢出索引,所有中文搜索全部落到英文版。

正确:canonical 必须永远指当前页自己

最短修复路径

Step 1:用一个 helper 集中生成 hreflang(不要手写)

// src/lib/hreflang.js
const BASE = "https://yourdomain.com";
const LANGS = ["en", "zh-Hans", "ja"];  // 你站点支持的所有语言

export function buildHreflang(currentLang, slug) {
  const canonical = `${BASE}/${currentLang}/${slug}/`;
  const alternates = LANGS.map((lang) => ({
    hreflang: lang,
    href: `${BASE}/${lang}/${slug}/`,
  }));
  alternates.push({
    hreflang: "x-default",
    href: `${BASE}/en/${slug}/`,
  });
  return { canonical, alternates };
}

在模板里:

---
import { buildHreflang } from "../lib/hreflang.js";
const { canonical, alternates } = buildHreflang(Astro.props.lang, Astro.props.slug);
---
<link rel="canonical" href={canonical} />
{alternates.map(({ hreflang, href }) => (
  <link rel="alternate" hreflang={hreflang} href={href} />
))}

这样每个页面 hreflang 数组完全一致,自动闭环。

Step 2:用爬虫扫整站验证闭环

// scripts/check-hreflang.mjs
import fg from "fast-glob";
import fs from "node:fs";

const files = fg.sync("dist/**/*.html");
const map = new Map();  // url -> Set of hreflang->url

for (const f of files) {
  const html = fs.readFileSync(f, "utf8");
  const canonical = html.match(/<link\s+rel=["']canonical["']\s+href=["']([^"']+)["']/i)?.[1];
  if (!canonical) continue;
  const alts = [...html.matchAll(/<link\s+rel=["']alternate["']\s+hreflang=["']([^"']+)["']\s+href=["']([^"']+)["']/gi)];
  map.set(canonical, new Map(alts.map(m => [m[1], m[2]])));
}

const issues = [];
for (const [url, alts] of map) {
  for (const [lang, altUrl] of alts) {
    if (lang === "x-default") continue;
    const reverseAlts = map.get(altUrl);
    if (!reverseAlts) issues.push(`${url} → ${altUrl} (${lang}): target not crawled`);
    else if (!reverseAlts.has(getMyLang(url))) issues.push(`${url} ↔ ${altUrl}: missing return tag`);
  }
}

function getMyLang(u) {
  return new URL(u).pathname.split("/")[1];  // /zh/foo → zh
}

console.log(issues.length ? issues.join("\n") : "All hreflang pairs closed ✓");

build 后跑一次,闭环检查。

Step 3:用第三方工具复核

Step 4:修完后等 7-21 天

  • Search Console → International Targeting 里错误数量下降需要 1-3 周
  • 期间可以”请求编入索引”主力 URL 加速

Step 5:如果 SC 长期没清空

  • 确认 Search Console 验证的域名包含所有语言子目录(不是只验了根域)
  • 检查 robots.txt 没把任何语言版本的目录屏蔽
  • 用浏览器 DevTools 看页面 head,confirm <link> 都在 head 内(不在 body 内才生效)

预防建议

  • hreflang 永远走一个 helper 函数生成,杜绝手写
  • canonical 一定指自己当前语言,不要跨语言指 canonical
  • 新增语言时,先全站补完 return tag 再发布,不要”先发英文版以后再补中文”
  • CI 加 hreflang 闭环检查脚本,缺一对就 build fail
  • 语言/地区码用 ISO 标准:语言 ISO 639-1、地区 ISO 3166-1 alpha-2、脚本 zh-Hans / zh-Hant

相关阅读

标签: #SEO #Google #Search Console #收录