Как создать многоязычные карты сайта в TanStack Start v1

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

Проблема

Карты сайта помогают поисковым системам находить и индексировать все страницы сайта. На многоязычном сайте с сотнями или тысячами страниц на каждом языке карты сайта могут быстро стать огромными. Один файл, в котором перечислены все URL для всех языков, становится неудобным и может превысить лимиты протокола карты сайта: 50 000 URL или 50 МБ. Если вы меняете структуру на одном языке, приходится пересоздавать и проверять весь файл заново. Большие монолитные карты сайта сложно поддерживать, они медленно обрабатываются и плохо масштабируются при добавлении новых языков или контента.

Решение

Разделите карту сайта на несколько файлов и используйте индексный файл карты сайта, чтобы отправлять сразу несколько карт. Создайте основной индекс карт сайта по адресу /sitemap.xml, который будет указывать на отдельные карты для каждого языка, например /sitemap-en.xml и /sitemap-es.xml. XML-формат индексного файла карты сайта похож на обычную карту сайта и определяется протоколом Sitemap. Такой подход делает отдельные файлы удобными для работы, позволяет обновлять каждый язык независимо и хорошо масштабируется при добавлении новых языков или страниц.

Шаги

1. Создайте помощник для генерации XML карты сайта

Напишите утилиту, которая будет генерировать корректный XML карты сайта из массива URL.

export function generateSitemapXML(urls: Array<{ loc: string; lastmod?: string; changefreq?: string; priority?: number }>): string {
const entries = urls.map(url => {
  let entry = `  <url>
    <loc>${url.loc}</loc>`
  if (url.lastmod) entry += `
    <lastmod>${url.lastmod}</lastmod>`
  if (url.changefreq) entry += `
    <changefreq>${url.changefreq}</changefreq>`
  if (url.priority !== undefined) entry += `
    <priority>${url.priority}</priority>`
  entry += `
  </url>`
  return entry
}).join('
')

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

Эта функция принимает массив объектов URL и возвращает правильно отформатированную XML-строку с нужным пространством имён и структурой.

2. Создайте помощник для генерации XML индексного файла карты сайта

Создайте вторую утилитную функцию, которая генерирует индекс карты сайта, указывающий на несколько дочерних карт сайта.

export function generateSitemapIndexXML(sitemaps: Array<{ loc: string; lastmod?: string }>): string {
const entries = sitemaps.map(sitemap => {
  let entry = `  <sitemap>
    <loc>${sitemap.loc}</loc>`
  if (sitemap.lastmod) entry += `
    <lastmod>${sitemap.lastmod}</lastmod>`
  entry += `
  </sitemap>`
  return entry
}).join('
')

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

Индекс карты сайта использует корневой тег <sitemapindex> и включает запись <sitemap> для каждой карты сайта, с дочерней записью <loc> для каждого родительского тега.

3. Определите серверный маршрут для основного индекса карты сайта

Создайте серверный маршрут по адресу /sitemap.xml, который возвращает индекс карты сайта со списком всех языковых карт сайта.

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapIndexXML } from "~/utils/sitemap";

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

export const Route = createFileRoute("/sitemap")({
  server: {
    handlers: {
      GET: async () => {
        const sitemaps = SUPPORTED_LOCALES.map((locale) => ({
          loc: `${BASE_URL}/sitemap-${locale}.xml`,
          lastmod: new Date().toISOString().split("T")[0],
        }));

        const xml = generateSitemapIndexXML(sitemaps);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

Этот маршрут генерирует индекс, который указывает на одну карту сайта для каждого языка и отдаёт его как XML с соответствующими заголовками кэширования.

4. Определите серверные маршруты для языковых карт сайта

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

import { createFileRoute } from "@tanstack/react-router";
import { generateSitemapXML } from "~/utils/sitemap";

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

async function getUrlsForLocale(locale: string) {
  return [
    { loc: `${BASE_URL}/${locale}`, changefreq: "daily", priority: 1.0 },
    {
      loc: `${BASE_URL}/${locale}/about`,
      changefreq: "monthly",
      priority: 0.8,
    },
    {
      loc: `${BASE_URL}/${locale}/contact`,
      changefreq: "monthly",
      priority: 0.8,
    },
  ];
}

export const Route = createFileRoute("/sitemap-$locale")({
  server: {
    handlers: {
      GET: async ({ params }) => {
        const { locale } = params;
        const urls = await getUrlsForLocale(locale);
        const xml = generateSitemapXML(urls);

        return new Response(xml, {
          headers: {
            "Content-Type": "application/xml",
            "Cache-Control": "public, max-age=3600",
          },
        });
      },
    },
  },
});

Серверные маршруты поддерживают динамические параметры пути так же, как TanStack Router, поэтому файл с именем $locale создаёт маршрут, принимающий динамический параметр локали. Каждая языковая карта сайта генерируется независимо и может обновляться без влияния на другие языки.

5. Получите URL-адреса из вашего источника данных

Замените функцию-заглушку getUrlsForLocale на логику, которая получает реальные URL-адреса из вашей базы данных, CMS или определений маршрутов.

async function getUrlsForLocale(locale: string) {
  const pages = await db.page.findMany({
    where: { locale, published: true },
    select: { slug: true, updatedAt: true },
  });

  return pages.map((page) => ({
    loc: `${BASE_URL}/${locale}/${page.slug}`,
    lastmod: page.updatedAt.toISOString().split("T")[0],
    changefreq: "weekly",
    priority: 0.7,
  }));
}

В этом примере из базы данных запрашиваются опубликованные страницы для заданной локали и сопоставляются с записями карты сайта с метаданными. Измените запрос и логику сопоставления в соответствии с вашей моделью данных и структурой URL.