hreflang 标签实现与多语言 sitemap
核心问题:hreflang 标签是什么?为什么没有它搜索引擎会惩罚多语言网站?如何正确实现?
真实场景
你的英文页面和中文页面内容相似(都是关于同一产品的),搜索引擎可能将它们视为重复内容,导致两个版本的排名都受损。hreflang 标签告诉 Google:"这些页面不是重复内容,它们是同一内容的不同语言版本,服务不同的用户群体。"
hreflang 是什么
hreflang 是 HTML <link> 元素的属性,用于指定同一内容在不同语言/地区版本之间的关系。
<!-- 在每个语言版本的页面 <head> 中添加所有版本的链接 -->
<head>
<!-- 自引用:当前页面 -->
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/products" />
<!-- 其他语言版本 -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
<link rel="alternate" hreflang="de-DE" href="https://example.com/de-DE/products" />
<link rel="alternate" hreflang="ja-JP" href="https://example.com/ja-JP/products" />
<!-- x-default:用于语言选择页或默认版本 -->
<link rel="alternate" hreflang="x-default" href="https://example.com/products" />
</head>
hreflang 语法规则
标签属性
<link rel="alternate"
hreflang="语言代码"
href="对应页面的绝对 URL" />
rel="alternate":表示这是当前页面的替代版本hreflang:BCP 47 语言标签(zh-CN、en-US、x-default)href:必须是绝对 URL(带协议和域名)
x-default 的用途
<!-- 场景 1:语言选择页(让用户选择语言)-->
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
<!-- 当用户的语言没有对应版本时,显示这个页面 -->
<!-- 场景 2:默认语言版本 -->
<link rel="alternate" hreflang="x-default" href="https://example.com/products" />
<!-- en 版本作为默认,当用户语言无匹配时使用 -->
在各框架中实现
Next.js App Router + next-intl
// src/app/[locale]/products/page.tsx
import { getTranslations } from 'next-intl/server';
import { locales } from '@/i18n/routing';
// Next.js 自动生成 alternate 链接(配合 next-intl)
export function generateMetadata({ params: { locale } }) {
// next-intl + Next.js 13+ 会自动生成 hreflang
// 只需配置 alternates
return {
alternates: {
canonical: `https://example.com/${locale}/products`,
languages: Object.fromEntries(
locales.map(l => [l, `https://example.com/${l}/products`])
),
},
};
}
// 或者在 layout.tsx 中全局配置
export const metadata = {
alternates: {
languages: {
'zh-CN': 'https://example.com/zh-CN',
'en-US': 'https://example.com/en-US',
'x-default': 'https://example.com',
},
},
};
// Next.js 会自动渲染为 <link rel="alternate" hreflang="..."> 标签
Nuxt 3 + @nuxtjs/i18n
// @nuxtjs/i18n 自动生成 hreflang 标签
// nuxt.config.ts
export default defineNuxtConfig({
i18n: {
baseUrl: 'https://example.com',
locales: [
{ code: 'zh-CN', iso: 'zh-CN' },
{ code: 'en-US', iso: 'en-US' },
{ code: 'de-DE', iso: 'de-DE' },
],
defaultLocale: 'zh-CN',
// 自动添加 hreflang
detectBrowserLanguage: false,
},
});
// 在页面中,@nuxtjs/i18n 自动在 <head> 中插入所有语言的 hreflang 标签
// 和 x-default(指向默认 locale)
手动实现(Express/FastAPI)
// 生成 hreflang 标签的工具函数
interface HreflangEntry {
hreflang: string;
href: string;
}
function generateHreflangTags(
currentPath: string, // 不含语言前缀的路径,如 /products
baseUrl: string,
locales: string[],
defaultLocale: string
): HreflangEntry[] {
const tags: HreflangEntry[] = locales.map(locale => ({
hreflang: locale,
href: `${baseUrl}/${locale}${currentPath}`,
}));
// x-default 指向默认语言版本(不带前缀)
tags.push({
hreflang: 'x-default',
href: `${baseUrl}${currentPath}`,
});
return tags;
}
// 在 Express 模板中使用
app.get('/:locale/products', (req, res) => {
const { locale } = req.params;
const hreflangTags = generateHreflangTags(
'/products',
'https://example.com',
['zh-CN', 'en-US', 'de-DE', 'ja-JP'],
'zh-CN'
);
res.render('products', { locale, hreflangTags });
});
<!-- 模板中渲染 hreflang 标签 -->
{{#each hreflangTags}}
<link rel="alternate" hreflang="{{this.hreflang}}" href="{{this.href}}" />
{{/each}}
多语言 Sitemap
Google 官方文档推荐的另一种实现方式:在 sitemap 中声明 hreflang 关系(与 HTML 标签二选一,推荐 sitemap 方式因为集中管理)。
Sitemap 格式
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- 每个 URL 出现 N 次(N = 语言版本数),每次列出所有语言版本 -->
<url>
<loc>https://example.com/zh-CN/products</loc>
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/products"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products"/>
<xhtml:link rel="alternate" hreflang="de-DE" href="https://example.com/de-DE/products"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products"/>
<lastmod>2026-03-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://example.com/en-US/products</loc>
<!-- 相同的 xhtml:link 集合 -->
<xhtml:link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/products"/>
<xhtml:link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products"/>
<xhtml:link rel="alternate" hreflang="de-DE" href="https://example.com/de-DE/products"/>
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/products"/>
<lastmod>2026-03-22</lastmod>
</url>
<!-- 继续其他语言版本和页面... -->
</urlset>
程序化生成 Sitemap(Next.js)
// src/app/sitemap.ts
import { MetadataRoute } from 'next';
const BASE_URL = 'https://example.com';
const LOCALES = ['zh-CN', 'en-US', 'de-DE', 'ja-JP'];
async function getAllProductSlugs(): Promise<string[]> {
// 从数据库获取所有商品 slug
const products = await db.products.findMany({ select: { slug: true } });
return products.map(p => p.slug);
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const productSlugs = await getAllProductSlugs();
const productUrls = productSlugs.flatMap(slug =>
LOCALES.map(locale => ({
url: `${BASE_URL}/${locale}/products/${slug}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.8,
alternates: {
languages: Object.fromEntries(
LOCALES.map(l => [l, `${BASE_URL}/${l}/products/${slug}`])
),
},
}))
);
const staticUrls = ['/products', '/about', '/contact'].flatMap(path =>
LOCALES.map(locale => ({
url: `${BASE_URL}/${locale}${path}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.6,
alternates: {
languages: Object.fromEntries(
LOCALES.map(l => [l, `${BASE_URL}/${l}${path}`])
),
},
}))
);
return [...staticUrls, ...productUrls];
}
分割 Sitemap(大型网站)
<!-- sitemap-index.xml:主索引文件 -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://example.com/sitemap-zh-CN.xml</loc>
<lastmod>2026-03-22</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-en-US.xml</loc>
<lastmod>2026-03-22</lastmod>
</sitemap>
<sitemap>
<loc>https://example.com/sitemap-products.xml</loc>
<lastmod>2026-03-22</lastmod>
</sitemap>
</sitemapindex>
常见的 hreflang 错误
错误一:缺少自引用
<!-- ❌ 错误:当前页面没有引用自己 -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/products" />
<!-- 当前页面是 de-DE 版本,但没有 de-DE 的自引用!-->
<!-- ✅ 正确:每个页面必须包含自引用 -->
<link rel="alternate" hreflang="de-DE" href="https://example.com/de-DE/products" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/products" />
<link rel="alternate" hreflang="x-default" href="https://example.com/products" />
错误二:hreflang 不对称
hreflang 必须双向引用:A 页面引用了 B,B 也必须引用 A。
<!-- ❌ 错误:en-US 页面没有反向引用 zh-CN -->
<!-- zh-CN 页面 -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
<!-- en-US 页面(缺少对 zh-CN 的引用)-->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
<!-- 忘记加 zh-CN 的链接!-->
<!-- ✅ 正确:所有版本相互引用 -->
<!-- 每个页面都列出所有语言版本 -->
错误三:使用相对 URL
<!-- ❌ 错误:相对 URL -->
<link rel="alternate" hreflang="en-US" href="/en-US/products" />
<!-- ✅ 正确:绝对 URL -->
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US/products" />
错误四:hreflang 语言代码错误
<!-- ❌ 常见错误 -->
<link rel="alternate" hreflang="zh_CN" href="..." /> <!-- 下划线不合法 -->
<link rel="alternate" hreflang="chinese" href="..." /> <!-- 不是 BCP 47 -->
<link rel="alternate" hreflang="cn" href="..." /> <!-- cn 是克里奥尔语!-->
<!-- ✅ 正确 -->
<link rel="alternate" hreflang="zh-CN" href="..." /> <!-- 简体中文(中国)-->
<link rel="alternate" hreflang="zh-TW" href="..." /> <!-- 繁体中文(台湾)-->
<link rel="alternate" hreflang="zh" href="..." /> <!-- 中文(不指定地区)-->
错误五:遗漏 x-default
<!-- ❌ 没有 x-default -->
<link rel="alternate" hreflang="zh-CN" href="..." />
<link rel="alternate" hreflang="en-US" href="..." />
<!-- ✅ 加上 x-default -->
<link rel="alternate" hreflang="zh-CN" href="..." />
<link rel="alternate" hreflang="en-US" href="..." />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
hreflang 验证工具
# Google Search Console → 国际化定向报告
# 提交 Sitemap 后查看 hreflang 错误
# 命令行检查(第三方工具)
npx hreflang-checker https://example.com/sitemap.xml
# 在线工具:
# - Ahrefs Site Audit
# - Screaming Frog SEO Spider(免费版可抓取 500 URL)
# - hreflang.org/testing-tool
下一节:hreflang 配置好了,最后一节处理 CDN 地理路由配置——如何让不同地区的用户访问最近的服务器,并在 Edge 层实现语言检测。