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 único archivo que liste todas las URL de todos los idiomas se vuelve difícil de manejar y puede exceder los límites de 50 000 URL o 50 MB definidos por el protocolo de sitemap. Cuando actualizas la estructura de un idioma, debes regenerar y revalidar el archivo completo. Los sitemaps monolíticos grandes son difíciles de mantener, lentos de procesar y no escalan 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 varios 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 al de un sitemap normal 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. Crea una función auxiliar 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 de 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 namespace y la estructura requeridos.
2. Crea una función auxiliar para generar XML de índice de 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 hija <loc> para cada etiqueta padre.
3. Define 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 de cada 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 encabezados de caché apropiados.
4. Define rutas de servidor para sitemaps específicos de cada idioma
Crea una ruta de servidor dinámica que genere un sitemap para cada idioma basándose en el parámetro de locale.
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 de 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 locale dinámico. Cada sitemap específico de idioma se genera de forma independiente y puede actualizarse sin afectar a otros idiomas.
5. Obtén URLs desde tu fuente de datos
Reemplaza la función de marcador de posición 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 locale dado y las mapea a entradas de sitemap con metadatos. Ajusta la consulta y la lógica de mapeo para que coincidan con tu modelo de datos y estructura de URLs.