最快修复: 这类警告几乎都是 return tag 缺失——A 页指向 B 页,但 B 页没回指 A 页。只要让每个语言版本的 <head> 输出完全相同的一组 <link rel="alternate" hreflang="...">(含自引用和 x-default),确认每个目标 URL 返回 HTTP 200,并确认每个页面的 <link rel="canonical"> 都指向自己、绝不跨语言,警告就会消失。
hreflang 是告诉 Google “这个页面在另一个语言/区域有等价版本,搜对应语言用户时请用那一版”。按 Google localized-versions 官方文档,它的规则严格到几乎无法靠手写正确:每个语言版本必须互相引用、且包含自身(return tag),语言码必须是 ISO 639-1,地区码必须是 ISO 3166-1 alpha-2,加上一个 x-default——任意一处漏写或错写,整组 hreflang 就失效。Google 原话:“If two pages don’t both point to each other, the tags will be ignored.”(两个页面若不互相指向,标签会被忽略。)
这些警告现在去哪看(这点变了): Search Console 的 “International Targeting” 报告——就是过去列出 “No return tags” 错误的那个页面——已在 2022 年 8 月被弃用并随后移除。截至 2026 年 6 月,Search Console 已经没有任何报告会标记 hreflang 问题。现在改用 URL Inspection 工具加第三方爬虫来诊断(见下文)。如果还有教程让你打开 “International Targeting”,那是过时信息。
常见原因
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” 的那个错误。
如何判断(2026 年 6 月): Search Console 已经不再报告这个,改用以下任一方式:
- 把两个 URL 输入 hreflang.org 在线检查器 互查闭环。
- 或者分别抓取两个页面、对比 hreflang 区块——两个页面输出的
<link rel="alternate">必须完全一致:
for u in /en/article /zh/article; do
echo "== $u =="
curl -s "https://yourdomain.com$u/" \
| grep -oE '<link[^>]*rel="alternate"[^>]*hreflang="[^"]*"[^>]*>'
done
# 两个区块应逐字相同(顺序不论)。
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 必须永远指向当前页、且同一语言。Google 文档明确要求:用 hreflang 时要 “specify a canonical page in the same language, or the best possible substitute language”(指定同语言的 canonical 页,或最接近的替代语言),绝不能跨语言。所以在 /zh/post 上,<link rel="canonical"> 必须是 https://yourdomain.com/zh/post/。
最短修复路径
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:用第三方工具复核
Search Console 已不再审计 hreflang,所以爬虫现在是主要的判断依据:
- Screaming Frog SEO Spider:免费版可爬 500 URL,有专门的 hreflang 标签页,一次性标出 return tag 缺失、canonical 不自引、语言码错误
- hreflang.org checker:输入两个 URL 快速核对 return tag(适合单点抽查)
- Ahrefs Site Audit 或 Sitebulb:付费,能报告整站的 hreflang 集群
Step 4:确认 Google 重新读过页面
现在没有 hreflang 报告可盯了,改用 Search Console 的 URL Inspection(网址检查)工具:
- 把某个语言版本的 URL 输入 URL Inspection → “Test live URL”(测试实际网址)。
- 在渲染后的 HTML / HTTP 响应里,确认
<link rel="alternate">都在、且 canonical 指向同语言。 - 对修好的 URL 点 “Request indexing”(请求编入索引)促使重新抓取。任何修复生效仍需大约 1-3 周让 Google 重抓整组——hreflang 只有在 Google 重新抓取过该集群里每一个页面后才会生效。
Step 5:重抓后仍不生效
- 确认验证的资源覆盖所有语言子目录(
domain资源覆盖全部;而https://yourdomain.com/en/这种网址前缀资源看不到/zh/)。 - 检查
robots.txt没屏蔽任何语言目录——被屏蔽的目标等同于 return tag 失效。 - 确认没有 HTTP 和 HTTPS(或 www 与非 www)同时在线;混用会导致 hreflang 目标和被索引的 URL 不一致,被判为 “no return tag”。
- 用浏览器 DevTools 看页面 head,确认
<link>都在<head>内——注入到<body>、或页面加载后由前端 JS 才插入的标签会被忽略。
预防建议
- hreflang 永远走一个 helper 函数生成,杜绝手写
- canonical 一定指自己当前语言,不要跨语言指 canonical
- 新增语言时,先全站补完 return tag 再发布,不要”先发英文版以后再补中文”
- CI 加 hreflang 闭环检查脚本,缺一对就 build fail
- 语言/地区码用 ISO 标准:语言
ISO 639-1、地区ISO 3166-1 alpha-2、脚本zh-Hans / zh-Hant
如何确认已修好
- 爬一遍构建后的站点(用 Screaming Frog 或上面的
scripts/check-hreflang.mjs)——报告里 return tag 缺失数和非 200 的 hreflang 目标数都必须为 0。 - 手动抽查一对:分别抓
/en/post/和/zh/post/,确认 hreflang 区块一致、且各自 canonical 同语言。 - 在 Search Console 用 URL Inspection 检查一个修好的页面 → “Test live URL” → 确认 alternate 链接渲染出来,并请求编入索引。
- 等 1-3 周。已经没有错误计数可盯了(International Targeting 报告已下线),真正的信号是:对应语言的搜索开始命中正确的本地化 URL。
常见问题
现在去 Search Console 哪里看 hreflang 错误? 看不到了。过去列出 “No return tags” 的 “International Targeting” 报告已在 2022 年 8 月被弃用并移除。截至 2026 年 6 月,Search Console 没有任何 hreflang 错误报告。改用爬虫(Screaming Frog)加 URL Inspection 工具。
hreflang="zh" 能用吗,还是必须 zh-Hans / zh-Hant?
只有一个中文版时,zh 本身合法、可用。一旦同时上线简体和繁体,就要分别用 zh-Hans 和 zh-Hant(脚本子标签),否则两版都抢同一个 zh 槽位,Google 会把两版都忽略。
需要 hreflang 自引用标签吗? 需要。Google 要求每个页面列出自身加所有其他语言版本。“no return tags” 的一个常见成因就是:给兄弟语言都加了 alternate,却漏了指向自己,导致集群永远不闭环。
页面之间互指了,警告却还在,为什么?
通常是肉眼看不出的不匹配:尾斜杠差异、http:// 与 https://(或 www 与非 www)主机不一致、或目标 301 跳转 / 被 noindex。这些都会让”回指”的 URL 和被索引的 URL 对不上,被 Google 判为 return tag 缺失。逐个 curl 目标,确认直接返回 200。
修好后多久警告消失? 大约 1-3 周。hreflang 只有在 Google 重抓过集群里每一个 URL 后才生效,所以最慢被重抓的那个页面决定节奏。对关键 URL “请求编入索引” 可以加速。
用 sitemap 里的 hreflang 还是 <head> 里的 <link>?
两者都行,但同一批 URL 别同时用两种(信号冲突)。小站用 <head> 标签最省事;上千个 URL 时 Google 推荐用 XML sitemap 的 <xhtml:link> 方式,因为它把所有标注集中管理。