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. 语言码 / 区域码写错
| 错写法 | 正确写法 | 说明 |
|---|---|---|
zh | zh 或 zh-Hans / zh-Hant | zh 自身是合法的,但简繁中文都用 zh 会被覆盖 |
cn | zh-CN | cn 是地区不是语言 |
tw | zh-TW | 同上 |
zh-hk | zh-HK | 大小写错误(Google 容忍,但有些工具不) |
en-us | en-US | 同上 |
en-uk | en-GB | UK 不是 ISO 国家码,GB 才是 |
zh-CN-Hans | zh-Hans-CN | 脚本码在前,地区码在后 |
jp | ja | 日语是 ja,jp 是国家码 |
如何判断:抓任意一个页面的所有 <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:用第三方工具复核
- hreflang.org checker:输入两个 URL,看 return tag 是否完整
- Screaming Frog:免费版可爬 500 URL,自带 hreflang audit
- Ahrefs Site Audit:付费但报告最详细
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