# General Translation Knowledge Base: Как создать карту сайта для многоязычного веб-сайта?
URL: https://generaltranslation.com/ru/kb/multilingual-sitemap.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Указывайте каждую языковую версию страницы как отдельную запись url. Если карта сайта содержит разметку hreflang, добавьте в каждую запись альтернативные версии xhtml:link для всех версий страницы, включая её саму, а также необязательный x-default. Разбивайте карту на несколько файлов, если она превышает 50 000 URL или 50 МБ. gt-next от General Translation назначает каждой локали собственный 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 в карте сайта так же, как hreflang в HTML, поэтому карта сайта может заменить соответствующие теги на страницах — при условии, что она полная и согласуется с каноническими URL.

## Где размещать hreflang: в карте сайта или в HTML?

В [руководстве Google по локализованным версиям](https://developers.google.com/search/docs/specialty/international/localized-versions) описаны три равноценных способа объявить языковые версии страниц. Там же сказано, что использование сразу нескольких способов не даёт никаких преимуществ в Поиске. Каждый языковой URL, который должен попасть в индекс, в любом случае нужно включить в карту сайта; выбирать приходится лишь то, где будут находиться аннотации hreflang.

| Способ                     | Где размещается                     | Когда подходит                                                                                               | На что обратить внимание                                    |
| -------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| HTML-теги `<link>`         | В `<head>` каждой страницы          | Фреймворк рендерит метаданные для каждой страницы, например через `alternates.languages` в Next.js           | Каждый шаблон должен выводить полный набор тегов            |
| HTTP-заголовок `Link`      | В заголовках ответа                 | Файлы не в формате HTML, например PDF                                                                        | Нужно настраивать сервер для каждого типа файла             |
| `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` для каждой версии.** Google учитывает его, только если он стабильно точен, — как дату последнего значимого изменения этого URL. Если немецкий перевод обновлён не в тот день, что английская страница, у него будет собственная дата.

Каждый файл может содержать не более 50 000 URL и занимать не более 50 МБ в несжатом виде. Дочерние элементы `xhtml:link` не учитываются в лимите URL, но учитываются в размере файла. При длине URL около 45 символов каждая альтернативная ссылка занимает примерно 100 байт, а запись с 20 языками — около 2,2 КБ. По нашей оценке, файл достигает 50 МБ примерно на 23 700 таких записей — задолго до 50 000.

### Разделение по локалям с помощью индекса карт сайта

Отдельный файл для каждой локали позволяет большинству сайтов держать файлы небольшими, но не гарантирует, что они уложатся в лимиты. Согласно приведённой выше оценке, локали, в которой больше примерно 23 700 страниц на 20 языках, одного файла уже не хватит, поэтому разделите её дальше, например на `sitemap-de-1.xml` и `sitemap-de-2.xml`. Чтобы Google прочитал все перечисленные в индексе файлы, достаточно отправить сам индекс. Отправлять каждый файл по отдельности необязательно: это лишь выделяет каждому из них отдельную строку в отчёте «Файлы Sitemap» в 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, карта сайта охватывает только URL-адреса в своём каталоге и ниже, если вы не отправите её через Search Console, а индекс может ссылаться только на карты сайта в своём каталоге или ниже.

## Как сгенерировать её в Next.js

Next.js 14.2 и более поздние версии формируют альтернативные версии `xhtml:link` на основе поля `alternates.languages` каждой записи в [`app/sitemap.ts`](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-localized-sitemap). В приведённом здесь варианте альтернативные версии строятся из единого списка локалей и локализованных слагов, поэтому каждая запись содержит ссылку на саму себя и на `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 страницы для конкретной локали — с локализованным слагом, если он есть.
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` — этот вызов применяет к ним percent-кодирование.
* **Разделение не создаёт индексный файл.** [`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 с перенаправлением.** Указывайте URL, по которому сервер отвечает кодом `200`. Со слагами из примера выше `/de/about` перенаправляет на `/de/ueber-uns`, поэтому в карту сайта следует включать только локализованный слаг.
* **Страницы с `noindex` или непереведённые страницы.** Карта сайта — это список URL, которые должны попасть в результаты поиска. Не включайте локаль, которая ещё не опубликована, и уберите её также из альтернативных версий в остальных записях.
* **`lastmod` со временем сборки.** Повторная генерация карты сайта не означает изменения контента. Указывайте дату последнего изменения каждой версии или вовсе опустите это поле.

## Как проверить карту сайта перед отправкой

Прежде чем отправлять карту сайта через отчёт «Файлы Sitemap» в Search Console или добавлять строку `Sitemap:` в `robots.txt`, выполните три проверки:

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 в этой схеме

`gt-next` от General Translation выделяет каждой локали собственный URL — именно такие 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) сопоставляет странице локализованный слаг, а [`localeRoutes`](/docs/react/nextjs/reference/functions/create-next-middleware#locale-routes) ограничивает набор страниц, доступных в локали. Храните эти настройки в одном модуле и импортируйте его и в промежуточное ПО, и в карту сайта — тогда карта сайта будет содержать ровно те 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, которые обслуживает промежуточное ПО.
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'], // страница с ценами ещё не переведена на бразильский португальский
};
```

```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 по-прежнему работает, поскольку промежуточное ПО перенаправляет `/pt-BR/pricing` на `/en/pricing` с кодом `307`, а 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>` и перечисляйте все URL, которые должны попасть в индекс, независимо от выбранного способа указания hreflang.
* В карте сайта с hreflang каждая запись перечисляет все версии страницы, включая её саму, причём набор версий одинаков во всех записях; `x-default` указывать необязательно.
* Один файл может содержать не более 50 000 URL и занимать не более 50 МБ; альтернативные версии не учитываются в лимите URL, но учитываются в размере, поэтому при большом количестве локалей раньше упираешься в лимит размера.
* Разделяйте карты сайта по локалям (а крупные локали — дополнительно) и объединяйте их в индекс карт сайта в корневом каталоге сайта; отправляйте индекс, предварительно проверив взаимность ссылок и коды статуса.
* При использовании `gt-next` задавайте общие `pathConfig` и `localeRoutes` для промежуточного ПО и `app/sitemap.ts`, чтобы карта сайта содержала ровно те URL, которые обслуживает ваше приложение.

## Related questions

- [Что такое hreflang и как его настроить?](https://generaltranslation.com/ru/kb/what-is-hreflang.mdx): Hreflang сообщает Google, какие URL являются языковыми или региональными версиями одной и той же страницы, чтобы в результатах поиска показывалась нужная. Добавьте его с помощью HTML-тегов link, HTTP-заголовков Link или карты сайта, сделайте все ссылки взаимными и используйте корректные коды, например en-GB, а не en-UK.
- [Как настроить SEO для многоязычного приложения Next.js?](https://generaltranslation.com/ru/kb/multilingual-nextjs-seo.mdx): Выделите каждому языку собственный URL с префиксом, например /en/about и /es/about, а затем укажите для каждого опубликованного языка html lang, канонический URL, ссылающийся на саму страницу, альтернативные версии hreflang с x-default, переведённые заголовок и описание, а также запись в карте сайта. Неопубликованные версии пометьте noindex. gt-next от General Translation направляет каждый язык на собственный URL и переводит заголовки с помощью getGT.
- [Языковые перенаправления и SEO: URL, cookie-файл или Accept-Language?](https://generaltranslation.com/ru/kb/locale-routing-seo.mdx): Язык каждой страницы, которую вы хотите видеть в индексе, должен определять URL, ведь Googlebot не отправляет заголовок Accept-Language и сканирует сайты в основном с американских адресов. Сохранённый в cookie-файле выбор, а за ним Accept-Language используйте только для того, чтобы решить, куда перенаправлять с нейтрального корневого пути вроде /, причём временным перенаправлением 302 или 307.
- [Подкаталог, поддомен или ccTLD для многоязычного SEO](https://generaltranslation.com/ru/kb/multilingual-url-structure.mdx): Используйте подкаталоги вида example.com/de/, если один продукт работает на общем сайте с единым процессом релизов. На страну указывает только ccTLD, поддомены позволяют размещать версии на отдельном хостинге, параметры ?lang= Google не рекомендует, а «имиджевые» домены вроде .io и .ai считает общими.
- [Как перевести SEO-заголовки и метаописания?](https://generaltranslation.com/ru/kb/translate-seo-metadata.mdx): Пишите заголовок и метаописание каждой языковой версии на её языке, используя слова, которые ищут пользователи на этом рынке, — они часто отличаются от дословного перевода. Затем локализуйте связанные с ними теги, включая Open Graph, canonical, hreflang, html lang и структурированные данные. Библиотека gt-next от General Translation переводит заголовки и описания внутри generateMetadata с помощью getGT.

## Previous and next questions

- Previous question: [Как переводить контент в Payload CMS?](https://generaltranslation.com/ru/kb/translate-payload-content.mdx)
- Next question: [Лучшие инструменты ИИ-перевода для разработчиков: API или платформы локализации](https://generaltranslation.com/ru/kb/best-ai-translation-tools.mdx)

## Sitemap

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