# General Translation Knowledge Base: 如何为多语言网站创建站点地图？
URL: https://generaltranslation.com/zh/kb/multilingual-sitemap.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 将每个页面的每个语言版本都列为独立的 url 条目。如果由站点地图承载 hreflang，则为每个条目添加指向所有版本（包括其自身）的 xhtml:link 替代项，并可选择添加 x-default；文件超过 50,000 个 URL 或 50MB 时需拆分。General Translation 的 gt-next 会为每个区域设置生成独立的 URL，供站点地图列出。
Category: 多语言 SEO
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

多语言站点地图会将页面的每个语言版本列为单独的 `<url>` 条目，因此 `/en/pricing`、`/de/preise` 和 `/pt-BR/precos` 共计三个条目。如果站点地图还声明了 hreflang，则每个条目都需以 `xhtml:link` 替代项的形式列出该页面的所有版本 (包括其自身) 。Google 对站点地图中的 hreflang 与 HTML 中的 hreflang 一视同仁，因此只要站点地图内容完整，且与你的规范网址保持一致，就可以用它取代页面中的标签。

## hreflang 应该放在站点地图中还是 HTML 中？

Google 的[本地化版本指南](https://developers.google.com/search/docs/specialty/international/localized-versions)介绍了三种等效的语言版本声明方式，并指出同时使用多种方式不会给搜索带来额外好处。所有希望被索引的语言 URL 仍然都应列入站点地图；你只需决定 hreflang 注释放在哪里。

| 方法                | 位置             | 适用场景                                           | 注意事项               |
| ----------------- | -------------- | ---------------------------------------------- | ------------------ |
| HTML `<link>` 标签  | 每个页面的 `<head>` | 框架按页面渲染元数据，例如 Next.js 的 `alternates.languages` | 每个模板都必须输出完整的标签集合   |
| HTTP `Link` 响应头   | 响应头            | PDF 等非 HTML 文件                                 | 需要按文件类型配置服务器       |
| 站点地图 `xhtml:link` | 单个或一组站点地图      | CMS 或构建步骤已掌握所有页面和区域设置，或者你无法编辑页面模板              | 未列入站点地图的页面不会获得任何注释 |

如果框架已经按页面渲染这些标签，就使用 HTML 标签；如果构建流程中有某处掌握所有页面及其区域设置，就使用站点地图。不要手动同时维护两者，否则两份列表迟早会对不上。 (标签语法和 HTTP 响应头的用法请参阅[什么是 hreflang](/kb/what-is-hreflang)。)

## 如何构建多语言站点地图

以下规则来自 Google 的[本地化版本指南](https://developers.google.com/search/docs/specialty/international/localized-versions)、[站点地图指南](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)以及 [sitemaps 协议](https://www.sitemaps.org/protocol.html)：

* **每个版本对应一个 `<url>`。** 一个页面有三种语言，就对应三个条目，每个条目都有各自的 `<loc>`。
* **每个条目都要列出所有版本，包括自身。** 同一页面所有条目中的 `xhtml:link` 集合完全相同；如果两个页面没有互相列出对方，Google 会忽略这对页面。
* **`x-default` 为可选项。** 如果有语言选择页，或有一个默认版本用于服务那些语言不在支持范围内的访问者，可以添加它。
* **声明命名空间。** `<urlset>` 需要包含 `xmlns:xhtml` 属性，其值为 `http://www.w3.org/1999/xhtml`。
* **使用最终的规范网址。** 必须是完整 URL (应写 `https://example.com/de/preise`，而非 `/de/preise`) ，返回 `200`，且未标记为 `noindex`。
* **使用有效代码。** 采用 ISO 639-1 语言代码，可选择附加 ISO 3166-1 alpha-2 地区代码 (`en-GB`) 或 ISO 15924 文字体系代码 (`zh-Hant`) 。
* **转义与编码。** 文件采用 UTF-8 编码，`&` 写作 `&amp;`，非 ASCII 路径需进行百分号编码，例如 `/de/über-uns` 应写为 `/de/%C3%BCber-uns`。
* **为每个版本单独设置 `lastmod`。** 该值表示对应 URL 最近一次重大更改的日期，且只有在始终准确的情况下，Google 才会采用。如果德语译文与英文页面不是同一天更新的，就应使用各自的日期。

每个文件最多包含 50,000 个 URL，未压缩大小不得超过 50MB。`xhtml:link` 子节点不计入 URL 数量限制，但会计入文件大小。假设 URL 长度约为 45 个字符，每个替代项约占 100 字节，一个包含 20 种语言的条目约为 2.2KB。据我们估算，文件在约 23,700 个这样的条目时就会达到 50MB，远在 50,000 个 URL 的上限之前。

### 使用站点地图索引按区域设置拆分

对大多数网站来说，每个区域设置使用一个文件就能让文件保持较小，但这并不能保证一定不超出限制。按上文的估算，如果某个区域设置在 20 种语言下有超过约 23,700 个页面，就需要多个文件，此时应进一步拆分，例如拆成 `sitemap-de-1.xml` 和 `sitemap-de-2.xml`。只需提交索引，Google 就能读取其中列出的所有文件；也可以选择另外单独提交每个文件，这样每个文件都会在 Search Console 的站点地图报告中单独占一行。[站点地图索引](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps)用于列出这些文件：

```xml title="sitemap.xml"
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-en.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-de.xml</loc>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-pt-br.xml</loc>
  </sitemap>
</sitemapindex>
```

每个区域设置文件只列出各自的 URL，但每个条目仍会包含该页面的全部替代项：

```xml title="sitemap-de.xml"
<?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>
    <loc>https://example.com/de/preise</loc>
    <lastmod>2026-10-03</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/pricing"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/preise"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/precos"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/pricing"/>
  </url>
  <url>
    <loc>https://example.com/de/ueber-uns</loc>
    <lastmod>2026-09-12</lastmod>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns"/>
    <xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/sobre"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
</urlset>
```

`sitemap-en.xml` 和 `sitemap-pt-br.xml` 的结构相同，只是 `<loc>` 和 `lastmod` 的值各不相同。请将这些文件放在站点根目录下：根据 Google 的说明，除非在 Search Console 中提交，否则站点地图只覆盖其所在目录及子目录下的 URL；同样，站点地图索引也只能引用其所在目录及子目录中的站点地图。

## 如何在 Next.js 中生成站点地图

Next.js 14.2 及更高版本会根据 [`app/sitemap.ts`](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap) 中每个条目的 `alternates.languages` 字段生成 `xhtml:link` 替代项。此实现基于同一份区域设置列表和本地化 slug 列表来构建替代项，因此每个条目都会包含其自身以及 `x-default`。这些列表以及每个区域设置提供哪些页面，都定义在 `i18n/routing.ts` 中；[General Translation 部分](#how-general-translation-fits)提供了一个读取 `gt.config.json` 的示例。

```ts title="app/sitemap.ts"
import type { MetadataRoute } from 'next';
import {
  BASE_URL,
  DEFAULT_LOCALE,
  LOCALES,
  localeRoutes,
  pathConfig,
} from '@/i18n/routing';

// 需要被索引的页面的共享路径。
const PAGES = ['/', '/about', '/pricing'];

// 只要 localeRoutes 未将页面排除，该区域设置就会提供此页面。
const serves = (locale: string, page: string) =>
  !localeRoutes[locale] || localeRoutes[locale].includes(page);

// 页面在某个区域设置下的公开 URL；若有本地化 slug，则使用该 slug。
function localeUrl(locale: string, page: string) {
  const path = pathConfig[page]?.[locale] ?? page;
  return `${BASE_URL}/${locale}${path === '/' ? '' : path}`;
}

export default function sitemap(): MetadataRoute.Sitemap {
  return PAGES.flatMap((page) => {
    const locales = LOCALES.filter((locale) => serves(locale, page));

    // 页面的所有版本（包括 <loc> 中的版本），外加 x-default。
    const languages: Record<string, string> = Object.fromEntries(
      locales.map((locale) => [locale, localeUrl(locale, page)]),
    );
    languages['x-default'] = localeUrl(DEFAULT_LOCALE, page);

    // 每个版本对应一个 <url>，且都带有相同的替代项。
    return locales.map((locale) => ({
      url: localeUrl(locale, page),
      alternates: { languages },
    }));
  });
}
```

使用 Next.js 16.4.3 和 `gt-next` 11.4.16 构建后，`/about` 页面生成了四个与下例类似的条目，每个条目都包含相同的五个链接：

```xml title="/sitemap.xml"
<url>
<loc>https://example.com/en/about</loc>
<xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<xhtml:link rel="alternate" hreflang="de" href="https://example.com/de/ueber-uns" />
<xhtml:link rel="alternate" hreflang="fr" href="https://example.com/fr/qui-sommes-nous" />
<xhtml:link rel="alternate" hreflang="pt-BR" href="https://example.com/pt-BR/about" />
<xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
</url>
```

Next.js 文档中有三个细节没有明确说明：

* **添加自引用。** Next.js 文档中的本地化示例只在英文 URL 下列出了 `es` 和 `de` 替代项，西班牙语和德语 URL 则没有各自的条目。而 Google 要求每个条目都列出自身，且每个语言版本都有独立的 `<url>`。
* **自行转义 URL。** 在我们的测试中，Next.js 16.4.3 会将 URL 原样写入 XML：查询字符串中的 `&` 会导致生成的文件无法通过 `xmllint` 校验，`ü` 也未经编码。请避免在站点地图 URL 中使用查询字符串，并通过 `new URL(path, BASE_URL).href` 构建路径，它会自动进行百分号编码。
* **拆分不会生成索引文件。** [`generateSitemaps`](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) 可以为每个区域设置返回一个 id，Next.js 会分别通过 `/sitemap/en.xml`、`/sitemap/de.xml` 等路径提供各个文件。在我们的测试中，此时访问 `/sitemap.xml` 会返回 404，因此请在 Search Console 中逐一提交这些文件。这样做还能解除 Google 对 `/sitemap/` 目录下文件的目录限制。

## 常见错误

* **缺少自引用或回链。** 如果 `/en/pricing` 列出了 `/de/preise`，但 `/de/preise` 没有列出 `/en/pricing`，Google 会忽略这一对页面。这通常是因为某个区域设置只添加到了部分页面，而其他页面遗漏了。
* **无效代码。** `en-UK` 不会生效，因为 `UK` 是保留代码；请改用 `en-GB`。`es-419` 虽然是有效的 BCP 47 代码，但 hreflang 不支持；请使用 `es` 或具体国家/地区的代码，例如 `es-MX`。
* **会重定向的 URL。** 应列出服务器返回 `200` 的 URL。以上文示例中的 slug 为例，`/de/about` 会重定向到 `/de/ueber-uns`，因此站点地图中只应包含本地化后的 slug。
* **`noindex` 或未翻译的页面。** 站点地图列出的是你希望出现在搜索结果中的 URL。尚未发布的区域设置应排除在外，同时也要从其他条目的替代项中将其移除。
* **将 `lastmod` 设为构建时间。** 重新生成站点地图并不算内容变更。请使用各版本最后一次修改的日期，或直接省略该字段。

## 提交前如何检查

在 Search Console 的“站点地图”报告中提交站点地图，或在 `robots.txt` 中添加 `Sitemap:` 行之前，请先完成以下三项检查：

1. **XML 格式正确：** `xmllint --noout sitemap-de.xml`。
2. **每个集群都完整且相互对应。** 以下脚本会读取一个或多个站点地图文件，并报告这些问题：未列出自身的条目、缺少自身条目的替代项、不一致的集合，以及格式错误的代码：

   ```python title="check_sitemap.py"
   import re
   import sys
   import xml.etree.ElementTree as ET

   NS = {
       's': 'http://www.sitemaps.org/schemas/sitemap/0.9',
       'x': 'http://www.w3.org/1999/xhtml',
   }
   # 语言、可选的文字体系、可选的地区：可匹配 es-419，但无法识别所有无效代码。
   CODE = re.compile(r'^[a-z]{2}(-[a-z]{4})?(-[a-z]{2})?$', re.IGNORECASE)
   RESERVED = ('-uk', '-eu', '-un')

   # 从每个站点地图文件中读取所有 <url>：loc -> {hreflang: href}。
   entries = {}
   for path in sys.argv[1:]:
       for url in ET.parse(path).getroot().findall('s:url', NS):
           loc = url.findtext('s:loc', namespaces=NS).strip()
           entries[loc] = {
               link.get('hreflang'): link.get('href')
               for link in url.findall('x:link', NS)
               if link.get('rel') == 'alternate'
           }

   problems = []
   for loc, alternates in entries.items():
       if alternates and loc not in alternates.values():
           problems.append(f'{loc} does not list itself')
       for lang, href in alternates.items():
           if lang == 'x-default':
               continue
           if not CODE.match(lang) or lang.lower().endswith(RESERVED):
               problems.append(f'{loc} uses an invalid hreflang code: {lang}')
           if href not in entries:
               problems.append(f'{loc} lists {href}, which has no <url> entry')
           elif entries[href] != alternates:
               problems.append(f'{href} does not list the same alternates as {loc}')

   print('\n'.join(problems) or f'{len(entries)} URLs, no problems found')
   sys.exit(1 if problems else 0)
   ```

   对所有区域设置文件一并运行该脚本：`python3 check_sitemap.py sitemap-en.xml sitemap-de.xml sitemap-pt-br.xml`；以上面的示例为例，它会输出 `6 URLs, no problems found`。
3. **每个 URL 都返回 `200`：**

   ```bash
   curl -s https://example.com/sitemap-de.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' |
     while read -r url; do echo "$(curl -s -o /dev/null -w '%{http_code}' "$url") $url"; done
   ```

## General Translation 的作用

General Translation 的 `gt-next` 为每个区域设置提供独立的 URL，而这正是多语言站点地图要列出的内容。[`createNextMiddleware`](/docs/react/nextjs/reference/functions/create-next-middleware) 负责处理 `/de/...` 和 `/pt-BR/...` 路径；`prefixDefaultLocale: true` 让源语言同样位于 `/en/` 下；[`pathConfig`](/docs/react/nextjs/reference/functions/create-next-middleware#path-config) 将页面映射到本地化的 slug；[`localeRoutes`](/docs/react/nextjs/reference/functions/create-next-middleware#locale-routes) 则限定某个区域设置可提供的页面。请将这些设置集中放在一个模块中，并在中间件和站点地图中分别导入，这样站点地图列出的 URL 就与中间件响应的 URL 完全一致：

```ts title="i18n/routing.ts"
import gtConfig from '../gt.config.json';

export const BASE_URL = 'https://example.com';
export const DEFAULT_LOCALE = gtConfig.defaultLocale;

// 你所支持的全部区域设置，取自 gt.config.json，源语言排在最前。
export const LOCALES = [...new Set([gtConfig.defaultLocale, ...gtConfig.locales])];

// 与 proxy.ts 共用，确保站点地图列出的 URL 与中间件提供的 URL 完全一致。
export const pathConfig: Record<string, Record<string, string>> = {
  '/about': { de: '/ueber-uns', fr: '/qui-sommes-nous' },
};
export const localeRoutes: Record<string, string[]> = {
  'pt-BR': ['/', '/about'], // pricing 页面尚未翻译成巴西葡萄牙语
};
```

```ts title="proxy.ts"
import { createNextMiddleware } from 'gt-next/middleware';
import { localeRoutes, pathConfig } from './i18n/routing';

export default createNextMiddleware({
  prefixDefaultLocale: true,
  pathConfig,
  localeRoutes,
});

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};
```

在 `gt.config.json` 中列出 `en`、`de`、`fr` 和 `pt-BR` 后，构建共生成了 11 个条目：三个页面乘以四个区域设置，再去掉巴西葡萄牙语的定价页。访问者仍可正常访问该 URL，因为中间件会以 `307` 将 `/pt-BR/pricing` 重定向到 `/en/pricing`，而会重定向的 URL 不应出现在站点地图中。所有列出的 URL 均返回 `200`，上面的检查器也未发现任何问题。`serves` 检查比较的是精确路径，因此如果你的 `localeRoutes` 使用了 `/blog/[[...slug]]` 之类的模式，请将其展开为要列出的具体页面。

如果你通过别名路由某个区域设置 (例如用 `/cn` 表示中文) ，请在 hreflang 键中使用规范代码，在 URL 中使用别名；[区域设置别名 SEO 指南](/docs/react/nextjs/locale-alias-seo#sitemap)借助 [`resolveCanonicalLocale`](/docs/react/nextjs/reference/functions/resolve-canonical-locale) 完成二者的转换。如需为这些页面提供翻译后的标题和描述，请参阅[如何翻译 SEO 标题和元描述？](/kb/translate-seo-metadata)。

## 要点总结

* 为每个语言版本单独设置一个 `<url>` 条目；无论采用哪种 hreflang 方法，都要列出所有希望被索引的 URL。
* 在包含 hreflang 的站点地图中，每个条目都要列出该页面的所有版本 (包括其自身) ，且各条目中的版本集合必须一致；`x-default` 为可选项。
* 单个文件最多包含 50,000 个 URL，且不超过 50MB；替代项不计入 URL 数量上限，但会计入文件大小，因此区域设置较多时，往往会先触及大小上限。
* 按区域设置拆分站点地图，内容较多的区域设置可再进一步拆分，并在站点根目录下通过站点地图索引统一管理；确认双向引用和状态码无误后，再提交该索引。
* 使用 `gt-next` 时，让中间件和 `app/sitemap.ts` 共享 `pathConfig` 和 `localeRoutes`，确保站点地图列出的 URL 与应用实际提供的 URL 完全一致。

## Related questions

- [什么是 hreflang？如何设置？](https://generaltranslation.com/zh/kb/what-is-hreflang.mdx): Hreflang 用于告诉 Google 哪些 URL 是同一页面的不同语言或区域版本，以便在搜索结果中展示正确的版本。你可以通过 HTML link 标签、HTTP Link 响应头或站点地图来添加它，确保所有链接相互指向，并使用有效的代码，例如 en-GB，而不是 en-UK。
- [如何为多语言 Next.js 应用设置 SEO？](https://generaltranslation.com/zh/kb/multilingual-nextjs-seo.mdx): 为每种语言提供带前缀的独立 URL，例如 /en/about 和 /es/about，然后为每种已发布的语言提供 html lang、自引用的规范网址、包含 x-default 的 hreflang 替代项、翻译后的 title 和 description，以及站点地图条目。将未发布的版本标记为 noindex。General Translation 的 gt-next 会将每种语言路由到各自的 URL，并通过 getGT 翻译标题。
- [语言重定向与 SEO：URL、cookie 还是 Accept-Language？](https://generaltranslation.com/zh/kb/locale-routing-seo.mdx): 凡是希望被搜索引擎收录的页面，都应由 URL 决定其语言，因为 Googlebot 不会发送 Accept-Language 请求头，而且大多从美国 IP 地址抓取。只有在决定 / 这类中性根路径重定向到哪里时，才应优先参考 cookie 中保存的用户选择，其次是 Accept-Language，并使用临时的 302 或 307 重定向。
- [多语言 SEO：子目录、子域名与 ccTLD 对比](https://generaltranslation.com/zh/kb/multilingual-url-structure.mdx): 如果同一产品共用一个站点和发布流程，请使用 example.com/de/ 这样的子目录。只有 ccTLD 能指明目标国家，子域名便于独立托管；Google 不建议使用 ?lang= 参数，并会将 .io、.ai 等个性化域名视为通用域名。
- [如何翻译 SEO 标题和meta 描述？](https://generaltranslation.com/zh/kb/translate-seo-metadata.mdx): 用各语言版本对应的语言撰写标题和meta 描述，并采用该市场用户实际搜索的词语——这些词往往与字面翻译不同。然后本地化与之相关的标签，包括 Open Graph、规范网址、hreflang、html lang 和结构化数据。General Translation 的 gt-next 可在 generateMetadata 中通过 getGT 翻译标题和描述。

## Previous and next questions

- Previous question: [如何为 TanStack Start 添加 i18n？](https://generaltranslation.com/zh/kb/add-i18n-to-tanstack-start.mdx)
- Next question: [如何使用 strings.xml 本地化 Android 应用？](https://generaltranslation.com/zh/kb/localize-android-app.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/zh/kb/sitemap.md) for every article.
