Saltar al contenido

El sitemap automático: filter, customPages y serialize

Generar el mapa del sitio sin mantenerlo a mano: instalar @astrojs/sitemap, reconocer el índice y los ficheros por lote que produce, y afinarlo con filter para excluir rutas, customPages para añadir URLs ajenas al árbol de ficheros y serialize para fijar changefreq, priority y lastmod, con sus diferencias entre SSG y SSR.

Instalar y qué genera

La integración se añade con npx astro add sitemap, que la registra en el array integrations de la configuración. Como el RSS, exige site: sin una URL base no hay direcciones absolutas que listar. Durante el build, la integración enumera todas las páginas conocidas y escribe un sitemap-index.xml que apunta a uno o varios sitemap-0.xml, troceados por lote cuando el sitio supera el límite de entradas por fichero.

JS
// astro.config.mjsimport { defineConfig } from 'astro/config';import sitemap from '@astrojs/sitemap';export default defineConfig({  site: 'https://misitio.com',  integrations: [sitemap()],});

El índice existe porque el estándar recomienda no meter decenas de miles de URLs en un solo fichero; la integración parte el listado en lotes y publica un índice que los agrupa. A ti te basta con declarar site y añadir la integración: el mapa aparece en /sitemap-index.xml tras el build, y solo queda enlazarlo desde el robots.txt o la cabecera para que los rastreadores lo encuentren.

Por defecto, la integración es deliberadamente parca: no inventa lastmod, changefreq ni priority para cada entrada, porque un valor fabricado engaña peor que su ausencia. El troceado se gobierna con entryLimit —45000 URLs por fichero, el techo que fija el estándar—, y esa es la razón de que hasta un sitio de tres páginas reciba un índice: la forma viene preparada para crecer a millones de URLs sin que reescribas nada.

filter y customPages

Dos opciones controlan qué URLs entran. filter recibe cada dirección como cadena y devuelve un booleano: true la conserva, false la descarta. Es la vía para sacar del mapa lo que no quieres indexar —un panel de administración, una carpeta de borradores, las páginas de agradecimiento tras un formulario—. customPages, al revés, añade URLs absolutas que no existen en el árbol de ficheros: páginas generadas por otro sistema, o rutas que el build no puede enumerar solo.

JS
sitemap({  filter: (page) => !page.includes('/borradores/'),  customPages: [    'https://misitio.com/externa/uno/',    'https://misitio.com/externa/dos/',  ],}),

Conviene ver el reparto: filter decide si una URL que ya existe entra en el mapa, mientras que customPages añade direcciones que el build jamás vería. Uno resta de lo conocido; el otro suma lo desconocido. Juntos tapan los dos huecos por los que un mapa automático se queda corto: rutas reales que no quieres indexar y rutas indexables que no nacen del árbol de ficheros.

serialize y la personalización por entrada

Cuando necesitas algo más fino que incluir o excluir, serialize recibe cada entrada del mapa —un objeto con url, changefreq, priority, lastmod y links— y devuelve la versión modificada. Ahí subes la prioridad de las páginas clave, marcas el índice del blog como de cambio diario, o derivas el lastmod de la fecha del contenido. Devolver undefined desde serialize descarta la entrada, lo que lo convierte también en un filtro con acceso al objeto completo.

JS
sitemap({  changefreq: 'weekly',  priority: 0.7,  serialize(item) {    if (item.url.endsWith('/blog/')) {      item.changefreq = 'daily';      item.priority = 1.0;    }    return item;  },}),

Las opciones changefreq, priority y lastmod a nivel de integración fijan los valores por defecto de todas las entradas; serialize los sobrescribe caso por caso. Conviene no obsesionarse con estos campos: los buscadores modernos los toman como pistas, no como órdenes, y un priority inflado en todas las páginas no engaña a nadie. Su valor real es relativo —señalar qué páginas importan más dentro de tu propio sitio—, no absoluto.

Un uso especialmente valioso de serialize es fijar lastmod con una fecha real tomada del frontmatter —la última edición de cada post—, para que el mapa diga cuándo cambió de verdad cada página y no una fecha global sin sentido. La regla es la de todo el nivel: deriva el dato de su fuente verdadera en vez de inventar un valor plausible.

SSG frente a SSR

El sitemap se construye en el build, y eso marca su alcance. En un sitio estático todas las páginas se conocen al compilar, así que la integración las enumera todas sin esfuerzo. En SSR solo se conocen las rutas prerenderizadas; las que se resuelven por petición —un [id] que sale de una base de datos en tiempo real— no existen aún cuando el build corre, y por tanto no entran en el mapa automáticamente. La solución es dárselas explícitas con customPages, o construir un endpoint de sitemap propio que las consulte.

Ese endpoint propio es el patrón de la lección anterior aplicado al XML: un src/pages/sitemap-extra.xml.ts que consulta tu fuente dinámica y emite el mapa a mano, del mismo modo que emites JSON. La integración cubre lo que el árbol de ficheros conoce; tu endpoint cubre lo que solo existe en tiempo de ejecución. Nada impide que ambos convivan, referenciados desde el mismo robots.txt, cada uno a cargo de la mitad del sitio que mejor conoce.

TypeScript
// src/pages/sitemap-extra.xml.tsimport type { APIRoute } from 'astro';export const GET: APIRoute = async ({ site }) => {  const ids = await cargarIdsDinamicos();  const urls = ids.map((id) => {    const loc = new URL('/p/' + id + '/', site).href;    return '<url><loc>' + loc + '</loc></url>';  });  const xml =    '<?xml version="1.0" encoding="UTF-8"?>' +    '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' +    urls.join('') +    '</urlset>';  return new Response(xml, {    headers: { 'Content-Type': 'application/xml' },  });};