Saltar al contenido

Un feed RSS desde una colección con @astrojs/rss

Publicar el contenido como feed sindicable: instalar @astrojs/rss, escribir un endpoint rss.xml que exporta GET, invocar rss() con title, description y site, y construir los items desde getCollection mapeando cada entrada tipada a title, pubDate, description y link, con contenido completo, stylesheet y customData.

El feed como endpoint

Un feed RSS es un documento XML servido en una URL, y en Astro eso es exactamente un endpoint. Se crea un fichero src/pages/rss.xml.js que exporta una función GET, y dentro se llama al ayudante rss() que trae el paquete @astrojs/rss: construye el XML bien formado y devuelve un Response listo. Se instala con npx astro add rss, o a mano con npm i @astrojs/rss.

TypeScript
// src/pages/rss.xml.jsimport rss from '@astrojs/rss';export function GET(context) {  return rss({    title: 'Mi blog',    description: 'Notas sobre Astro y la web',    site: context.site,    items: [],  });}

El context.site lee la clave site de tu astro.config, y es obligatoria: un feed necesita URLs absolutas para que un lector remoto sepa a dónde apunta cada entrada, y sin site esas direcciones no pueden construirse. Como cualquier endpoint estático, este se ejecuta una sola vez en el build y congela su salida en un fichero rss.xml; no hay servidor vivo detrás, solo XML prehorneado.

La extensión doble del nombre —.xml.js— cumple dos papeles: fija la URL final del feed y le avisa a Astro de que esto es un endpoint, no una página. En un sitio por lo demás dinámico conviene marcarlo con export const prerender = true, porque un feed rara vez necesita regenerarse en cada petición: prehornearlo lo sirve más rápido y más barato, y deja el cómputo bajo demanda para lo que de verdad cambia entre visitas.

rss() y los items desde getCollection

El array items es donde entra el contenido, y su fuente natural es la colección. getCollection('blog') devuelve las entradas ya validadas y tipadas; basta con mapear cada una a la forma que rss() espera. Como data conoce su tipo, ese mapeo es seguro: el compilador vigila que pubDate sea una fecha y que title sea texto.

TypeScript
import rss from '@astrojs/rss';import { getCollection } from 'astro:content';export async function GET(context) {  const posts = await getCollection('blog', ({ data }) => !data.draft);  return rss({    title: 'Mi blog',    description: 'Notas sobre Astro y la web',    site: context.site,    items: posts.map((post) => ({      title: post.data.title,      pubDate: post.data.pubDate,      description: post.data.description,      link: `/blog/${post.id}/`,    })),  });}

El link se escribe relativo; rss() lo resuelve contra site hasta una URL absoluta. El pubDate debe ser un Date, algo que el esquema de la colección ya garantiza —de nuevo, el tipado evita el error antes de que exista—. Los cuatro campos de cada item son el contrato mínimo del formato.

La barra final del link no es un capricho: debe coincidir con cómo sirve tu sitio esas URLs, algo que gobierna la opción trailingSlash de la configuración. Un feed cuyos enlaces no casan con las direcciones reales manda a los lectores a redirecciones o a errores. Y aunque muchos agregadores reordenan por su cuenta, pasar los items ya ordenados por pubDate descendente deja el XML coherente por sí mismo, sin depender de la cortesía del cliente.

Contenido completo, stylesheet y customData

Un item admite más que el contrato mínimo. Puede llevar categories para las etiquetas, author, un bloque content con el HTML completo del cuerpo, y customData, una cadena de XML crudo para campos que el formato no cubre. Para un feed que muestre el artículo entero, el content debe ser HTML ya renderizado a texto; @astrojs/rss no ejecuta tus componentes de MDX, así que el cuerpo se compila aparte con una librería como markdown-it y se limpia con sanitize-html.

TypeScript
return rss({  title: 'Mi blog',  description: 'Notas sobre Astro y la web',  site: context.site,  items: posts.map((post) => ({    title: post.data.title,    pubDate: post.data.pubDate,    description: post.data.description,    link: `/blog/${post.id}/`,    categories: post.data.tags,    author: post.data.author,  })),  stylesheet: '/rss/estilos.xsl',  customData: `<language>es-es</language>`,});

La opción stylesheet apunta a una hoja XSL que da al XML una vista legible para humanos que abran la URL en el navegador. El customData a nivel de feed añade metadatos del canal, como el idioma. Y si tus páginas de Markdown viven bajo src/pages en vez de en una colección, el ayudante pagesGlobToRssItems construye los items directamente desde un import.meta.glob, sin mapear a mano.

Para incrustar el cuerpo completo como content:encoded, el feed necesita declarar antes el espacio de nombres correspondiente con la opción xmlns; sin esa declaración, un lector estricto ignora el contenido enriquecido. Es el rigor de siempre del XML: cada extensión del formato se anuncia antes de usarse. Ese mismo cuidado —cabecera Content-Type correcta, enlaces absolutos, namespaces declarados— es lo que separa un feed que todos los lectores entienden de uno que funciona solo en el tuyo.