Cómo crear sitemaps multilingües en TanStack Start v1
Organiza sitemaps por idioma para escalar
Problema
Los sitemaps ayudan a los motores de búsqueda a descubrir y rastrear todas las páginas de un sitio. Un sitio multilingüe con cientos o miles de páginas por idioma puede generar rápidamente sitemaps enormes. Un solo archivo que enumere todas las URL para cada idioma se vuelve difícil de manejar y puede exceder los límites de 50.000 URL o 50MB definidos por el protocolo de sitemap. Cuando actualizas la estructura de un idioma, debes regenerar y revalidar todo el archivo. Los sitemaps monolíticos grandes son difíciles de mantener, lentos de procesar y no escalan bien a medida que añades idiomas o contenido.
Solución
Divide tu sitemap en múltiples archivos y utiliza un archivo índice de sitemap para enviar muchos sitemaps a la vez. Crea un índice de sitemap de nivel superior en /sitemap.xml que apunte a sitemaps específicos por idioma como /sitemap-en.xml y /sitemap-es.xml. El formato XML de un archivo índice de sitemap es similar a un sitemap regular y está definido por el Protocolo de Sitemap. Esto mantiene los archivos individuales manejables, te permite actualizar cada idioma de forma independiente y escala bien a medida que añades nuevos idiomas o páginas.
Pasos
1. Crear un helper para generar XML de sitemap
Construye una función de utilidad que genere XML de sitemap válido a partir de un array de entradas 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>`
}
Esta función acepta un array de objetos URL y devuelve una cadena XML correctamente formateada con el espacio de nombres y la estructura requeridos.
2. Crear un helper para generar el XML del índice del sitemap
Construye una segunda función de utilidad que genere un índice de sitemap que apunte a múltiples sitemaps secundarios.
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>`
}
El índice del sitemap utiliza una etiqueta raíz <sitemapindex> e incluye una entrada <sitemap> para cada sitemap, con una entrada secundaria <loc> para cada etiqueta principal.
3. Definir una ruta de servidor para el índice principal del sitemap
Crea una ruta de servidor en /sitemap.xml que devuelva el índice del sitemap listando todos los sitemaps específicos por idioma.
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",
},
});
},
},
},
});
Esta ruta genera un índice que apunta a un sitemap por idioma y lo sirve como XML con los encabezados de caché apropiados.
4. Definir rutas de servidor para sitemaps específicos por idioma
Crea una ruta de servidor dinámica que genere un sitemap para cada idioma basado en el parámetro de localización.
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",
},
});
},
},
},
});
Las rutas del servidor admiten parámetros de ruta dinámicos de la misma manera que TanStack Router, por lo que un archivo nombrado con $locale crea una ruta que acepta un parámetro de localización dinámico. Cada sitemap específico por idioma se genera de forma independiente y puede actualizarse sin afectar a otros idiomas.
5. Obtener URLs desde tu fuente de datos
Reemplaza la función placeholder getUrlsForLocale con lógica que obtenga URLs reales desde tu base de datos, CMS o definiciones de rutas.
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,
}));
}
Este ejemplo consulta una base de datos para páginas publicadas en el idioma especificado y las mapea a entradas del sitemap con metadatos. Ajusta la consulta y la lógica de mapeo para que coincidan con tu modelo de datos y estructura de URL.