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
声明了 en 和 zh 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 错误报告