Sitemap для разных языков

Многоязычные приложения требуют особого подхода к поисковой оптимизации. Если сайт доступен на нескольких языках, поисковые системы должны понимать:

  • какие версии страниц существуют;
  • на каком языке представлена каждая версия;
  • какие страницы являются альтернативами друг другу;
  • какие URL необходимо индексировать.

Даже при корректной настройке тегов hreflang поисковым системам полезно получать информацию о структуре многоязычного сайта через Sitemap. Для проектов, использующих I18next, генерация карты сайта обычно строится на основе того же списка языков, который используется системой локализации.


Связь I18next и Sitemap

Типичная конфигурация I18next содержит список поддерживаемых языков:

i18next.init({
  lng: "en",
  fallbackLng: "en",

  supportedLngs: [
    "en",
    "de",
    "fr",
    "es"
  ]
});

Этот список становится источником данных для генерации многоязычного Sitemap.

Предположим, приложение содержит следующие маршруты:

const routes = [
  "/",
  "/about",
  "/products",
  "/contacts"
];

Для каждого маршрута необходимо сформировать URL на всех поддерживаемых языках.

Результат:

https://example.com/en/
https://example.com/en/about

https://example.com/de/
https://example.com/de/about

https://example.com/fr/
https://example.com/fr/about

Таким образом, Sitemap фактически представляет собой произведение:

Маршруты × Языки

Структура URL в многоязычном приложении

Язык в пути

Наиболее распространённый вариант:

/en/about
/de/about
/fr/about

Конфигурация I18next:

i18next.init({
  supportedLngs: ["en", "de", "fr"]
});

Генерация Sitemap выполняется относительно языковых префиксов.


Язык в поддомене

Другой популярный вариант:

en.example.com
de.example.com
fr.example.com

В этом случае карта сайта содержит разные домены:

<url>
  <loc>https://en.example.com/about</loc>
</url>

<url>
  <loc>https://de.example.com/about</loc>
</url>

Язык через отдельные домены

Крупные международные проекты могут использовать отдельные доменные зоны:

example.com
example.de
example.fr

В этом случае генератор Sitemap должен учитывать соответствие языка и домена.

Пример конфигурации:

const domains = {
  en: "https://example.com",
  de: "https://example.de",
  fr: "https://example.fr"
};

Формирование списка языков из конфигурации I18next

Чтобы не дублировать информацию, языки желательно получать непосредственно из конфигурации локализации.

Пример:

export const supportedLanguages = [
  "en",
  "de",
  "fr",
  "es"
];

Использование в I18next:

i18next.init({
  supportedLngs: supportedLanguages
});

Использование при генерации Sitemap:

import { supportedLanguages } from "./languages.js";

Единый источник данных исключает рассинхронизацию между переводами и SEO-настройками.


Простая генерация Sitemap

Исходные данные:

const languages = [
  "en",
  "de",
  "fr"
];

const routes = [
  "",
  "about",
  "products",
  "contacts"
];

Генерация URL:

const urls = [];

for (const lng of languages) {
  for (const route of routes) {
    urls.push(
      `https://example.com/${lng}/${route}`
    );
  }
}

Полученный массив:

[
  "https://example.com/en/",
  "https://example.com/en/about",
  "https://example.com/de/",
  "https://example.com/de/about"
]

Генерация XML

Создание элементов Sitemap:

const sitemap = urls
  .map(
    url => `
<url>
  <loc>${url}</loc>
</url>`
  )
  .join("");

Формирование итогового XML:

const xml = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemap}
</urlset>
`;

Сохранение файла:

import fs from "fs";

fs.writeFileSync(
  "public/sitemap.xml",
  xml
);

Использование hreflang в Sitemap

Для многоязычных сайтов рекомендуется указывать альтернативные языковые версии прямо внутри Sitemap.

Пример для страницы:

/en/about
/de/about
/fr/about

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/about"/>

  <xhtml:link
      rel="alternate"
      hreflang="fr"
      href="https://example.com/fr/about"/>
</url>

Такая запись сообщает поисковому роботу, что страницы являются переводами друг друга.


Добавление пространства имён XHTML

При использовании hreflang необходимо расширить корневой элемент:

<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">

Без этого ссылки альтернативных языков будут считаться некорректными.


Автоматическая генерация hreflang

Исходные данные:

const languages = [
  "en",
  "de",
  "fr"
];

Функция генерации альтернативных ссылок:

function createAlternateLinks(route) {
  return languages
    .map(
      lng => `
<xhtml:link
rel="alternate"
hreflang="${lng}"
href="https://example.com/${lng}/${route}" />`
    )
    .join("");
}

Формирование узла URL:

function createUrlNode(route) {
  return `
<url>
  <loc>
    https://example.com/en/${route}
  </loc>

  ${createAlternateLinks(route)}
</url>`;
}

Поддержка fallback-языка

В I18next часто используется резервный язык:

i18next.init({
  fallbackLng: "en"
});

Обычно английская версия считается основной.

Для неё рекомендуется указывать дополнительное значение:

<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://example.com/en/about"/>

Это сообщает поисковой системе URL по умолчанию.

Генерация:

function createDefaultLink(route) {
  return `
<xhtml:link
rel="alternate"
hreflang="x-default"
href="https://example.com/en/${route}" />
`;
}

Исключение непереведённых страниц

Не каждая страница доступна на всех языках.

Пример:

const pages = [
  {
    route: "about",
    languages: ["en", "de", "fr"]
  },
  {
    route: "legal",
    languages: ["en"]
  }
];

Генерация выполняется только для существующих переводов:

for (const page of pages) {
  for (const lng of page.languages) {
    // создание URL
  }
}

Это предотвращает появление в Sitemap страниц с ошибкой 404.


Проверка наличия переводов

Иногда данные о переводах можно получать непосредственно из структуры локализации.

Пример файлов:

locales
 ├─ en
 │   ├─ common.json
 │   └─ pages.json
 ├─ de
 │   ├─ common.json
 │   └─ pages.json
 └─ fr
     └─ common.json

Если отсутствует:

fr/pages.json

страницы соответствующего раздела можно исключить из французской версии Sitemap.


Генерация Sitemap в Next.js с I18next

Конфигурация локализации:

module.exports = {
  i18n: {
    locales: ["en", "de", "fr"],
    defaultLocale: "en"
  }
};

Получение языков:

const locales = [
  "en",
  "de",
  "fr"
];

Формирование URL:

const urls = locales.flatMap(locale =>
  routes.map(
    route =>
      `https://example.com/${locale}${route}`
  )
);

Генерация может выполняться во время сборки проекта.


Генерация Sitemap в Express

Создание маршрута:

app.get("/sitemap.xml", (req, res) => {
  res.header(
    "Content-Type",
    "application/xml"
  );

  res.send(xml);
});

При большом количестве страниц XML может формироваться динамически на основе данных базы.


Разделение Sitemap на несколько файлов

По стандарту один Sitemap ограничен:

50 000 URL
или
50 МБ

Для крупных многоязычных проектов число страниц быстро растёт.

Пример:

10 000 страниц
×
8 языков
=
80 000 URL

Необходимо разделение:

sitemap-en.xml
sitemap-de.xml
sitemap-fr.xml

Индексный файл:

<sitemapindex>
  <sitemap>
    <loc>
      https://example.com/sitemap-en.xml
    </loc>
  </sitemap>

  <sitemap>
    <loc>
      https://example.com/sitemap-de.xml
    </loc>
  </sitemap>
</sitemapindex>

Генерация отдельных Sitemap для каждого языка

Исходный список языков:

const languages = [
  "en",
  "de",
  "fr"
];

Создание файлов:

for (const lng of languages) {
  generateLanguageSitemap(lng);
}

Функция:

function generateLanguageSitemap(lng) {
  const urls = routes.map(
    route =>
      `https://example.com/${lng}/${route}`
  );

  // формирование XML
}

Подобная схема хорошо масштабируется при увеличении числа языков.


Динамические маршруты

Многие сайты используют данные из базы:

const products = [
  {
    slug: "iphone-16"
  },
  {
    slug: "galaxy-s25"
  }
];

Генерация:

for (const product of products) {
  urls.push(
    `/products/${product.slug}`
  );
}

Для каждого языка создаются собственные URL:

https://example.com/en/products/iphone-16
https://example.com/de/products/iphone-16
https://example.com/fr/products/iphone-16

Локализованные URL

Иногда переводится не только контент, но и сам путь.

Пример:

/en/products
/de/produkte
/fr/produits

Конфигурация:

const localizedRoutes = {
  en: {
    products: "products"
  },
  de: {
    products: "produkte"
  },
  fr: {
    products: "produits"
  }
};

Генерация:

const route =
  localizedRoutes[lng].products;

const url =
  `https://example.com/${lng}/${route}`;

Такая структура повышает качество международного SEO.


Актуализация Sitemap после обновления переводов

При добавлении нового языка:

supportedLngs: [
  "en",
  "de",
  "fr",
  "it"
]

карта сайта должна пересоздаваться автоматически.

Типичный процесс CI/CD:

1. Добавление переводов
2. Сборка проекта
3. Генерация Sitemap
4. Деплой

Благодаря этому Sitemap всегда соответствует текущей конфигурации I18next.


Проверка корректности многоязычного Sitemap

После генерации необходимо убедиться, что:

  • каждая языковая версия существует;
  • отсутствуют ссылки на 404-страницы;
  • все URL используют корректный язык;
  • hreflang соответствует фактическому языку страницы;
  • присутствует x-default;
  • каждая группа альтернативных страниц полностью связана между собой;
  • Sitemap содержит только канонические URL;
  • все языки из конфигурации I18next учтены при генерации.

Согласованность между настройками I18next, маршрутизацией приложения и картой сайта обеспечивает корректную индексацию локализованных страниц и помогает поисковым системам правильно определять языковые версии контента.