Как создать многоязычные карты сайта в Next.js (Pages Router) v16

Организация карт сайта по языкам для масштабирования

Проблема

Карта сайта помогает поисковым системам находить и индексировать страницы сайта. Для многоязычного сайта с сотнями или тысячами страниц на каждом языке одна общая карта сайта, включающая все URL для всех локалей, быстро становится неудобной. Большие монолитные карты сайта могут превышать лимиты протокола карты сайта — 50 000 URL или 50 МБ, из-за чего становятся недействительными. Даже если размер остается в пределах лимита, пересоздавать и проверять огромный файл при каждом изменении контента на одном языке неэффективно. По мере роста сайта и добавления новых языков или страниц такой подход не масштабируется.

Решение

Организуйте карты сайта в иерархию с помощью индексного файла карты сайта. В индексном файле перечисляются отдельные карты сайта для каждого языка, каждая из которых содержит URL только для одной локали. Такая структура позволяет держать отдельные файлы карты сайта в рамках протокольных ограничений. При изменении контента на одном языке нужно пересоздавать только карту сайта для этого языка. Такой подход легко масштабируется — для каждого нового языка создается отдельная карта сайта, которая добавляется в индекс. Поисковые системы сначала обходят индекс, а затем переходят по ссылкам на карты сайта отдельных языков.

Шаги

1. Создайте индексную страницу карты сайта

Создайте страницу в директории pages, чтобы динамически генерировать индекс карты сайта с помощью getServerSideProps.

import { GetServerSideProps } from "next";

const SITE_URL = "https://example.com";
const LOCALES = ["en", "es", "fr", "de"];

function generateSitemapIndex(locales: string[]): string {
  const sitemapEntries = locales
    .map((locale) => {
      return `
<sitemap>
  <loc>${SITE_URL}/sitemap-${locale}.xml</loc>
  <lastmod>${new Date().toISOString()}</lastmod>
</sitemap>`;
    })
    .join("");

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

export const getServerSideProps: GetServerSideProps = async ({ res }) => {
  const sitemap = generateSitemapIndex(LOCALES);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function SitemapIndex() {}

В индексе используется корневой элемент <sitemapindex> с записями <sitemap>, каждая из которых содержит дочерний элемент <loc> со ссылкой на карту сайта для определенного языка. Функция getServerSideProps устанавливает заголовок Content-Type в значение text/xml и напрямую записывает XML-ответ.

2. Создайте страницы карты сайта для каждого языка

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

import { GetServerSideProps } from "next";

const SITE_URL = "https://example.com";

interface PageData {
  slug: string;
  lastModified: string;
}

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  return [
    { slug: "about", lastModified: "2024-01-15" },
    { slug: "contact", lastModified: "2024-01-20" },
  ];
}

function generateSitemap(locale: string, pages: PageData[]): string {
  const urlEntries = pages
    .map((page) => {
      return `
<url>
  <loc>${SITE_URL}/${locale}/${page.slug}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

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

export const getServerSideProps: GetServerSideProps = async ({
  params,
  res,
}) => {
  const locale = params?.locale as string;

  const pages = await getPagesByLocale(locale);
  const sitemap = generateSitemap(locale, pages);

  res.setHeader("Content-Type", "text/xml");
  res.write(sitemap);
  res.end();

  return {
    props: {},
  };
};

export default function LocaleSitemap() {}

Динамический маршрут извлекает локаль из параметров URL и генерирует XML, содержащий только URL-адреса для этого языка. Каждая карта сайта содержит только контент одной локали.

3. Получите контент для конкретной локали

Замените функцию-заглушку getPagesByLocale на ваш реальный источник данных.

async function getPagesByLocale(locale: string): Promise<PageData[]> {
  const response = await fetch(
    `https://api.example.com/pages?locale=${locale}`,
  );
  const data = await response.json();

  return data.pages.map((page: any) => ({
    slug: page.slug,
    lastModified: page.updatedAt,
  }));
}

Эта функция обращается к вашей CMS, базе данных или API, чтобы получить страницы для указанной локали. Она возвращает структурированные данные, которые генератор карты сайта преобразует в XML-записи.

4. Добавьте статические страницы в каждую карту сайта

Добавьте статические маршруты, которые есть на всех языках, вместе с динамическим контентом.

function generateSitemap(locale: string, pages: PageData[]): string {
  const staticPages = [
    { slug: "", lastModified: new Date().toISOString() },
    { slug: "about", lastModified: new Date().toISOString() },
  ];

  const allPages = [...staticPages, ...pages];

  const urlEntries = allPages
    .map((page) => {
      const path = page.slug ? `/${locale}/${page.slug}` : `/${locale}`;
      return `
<url>
  <loc>${SITE_URL}${path}</loc>
  <lastmod>${page.lastModified}</lastmod>
</url>`;
    })
    .join("");

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

Объединяя статические и динамические страницы, вы обеспечиваете полноту карты сайта для каждого языка. Для статических страниц используется текущее время как дата последнего изменения.

5. Укажите индекс в robots.txt

Добавьте расположение индекса карты сайта в файл robots.txt, чтобы поисковые системы могли его найти.

User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

Достаточно указать только файл индекса — поисковые системы автоматически перейдут по ссылкам на отдельные языковые карты сайта. Разместите этот файл в директории public.