wandres.dev
RSS, SITEMAP Y DATOS · feeds y datos derivados

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.

⏱ 15 min

El RSS es la forma más antigua de datos derivados: el mismo contenido que pintas como HTML, proyectado ahora como XML para lectores y agregadores. No es una segunda copia que mantener, sino otra lectura de la colección que ya tienes. Con @astrojs/rss, un feed deja de ser un fichero que se edita a mano y pasa a ser un endpoint que se genera del contenido en cada build: si añades un post, el feed lo incluye solo. Sindicar tu sitio se reduce a mapear entradas tipadas a la forma que un lector espera.

🎯 Al terminar esta lección sabrás
  • Instalar @astrojs/rss y crear el endpoint que sirve el feed.
  • Generar el XML con rss() a partir de title, description y site.
  • Construir los items desde getCollection, mapeando cada entrada tipada.
  • Enriquecer el feed 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.

// src/pages/rss.xml.js
import 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.

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.

🏷️

title

El titular de la entrada. Suele venir directo de post.data.title, sin transformación.

📅

pubDate

La fecha de publicación como Date. Los agregadores ordenan y agrupan por ella.

✍️

description

El resumen que se muestra en el lector. Un extracto, no el cuerpo entero.

🔗

link

La URL de la entrada, relativa; rss() la vuelve absoluta contra site.

ℹ️
El feed reutiliza la misma colección

Repara en que getCollection('blog') es la misma llamada que alimenta la portada y las páginas de detalle. No hay una fuente distinta para el feed: hay una fuente y varias proyecciones. Filtrar los borradores aquí con la función de filtro mantiene el feed limpio sin tocar el contenido, y ordenar por pubDate descendente lo deja como lo espera un lector, aunque muchos reordenan por su cuenta.

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.

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.

flowchart LR
GC[getCollection blog] --> MAP[mapear a items]
MAP --> RSS[rss helper]
RSS --> RES[Response con XML]
RES --> FILE[fichero rss xml]
style GC fill:#89b4fa,color:#11111b
style FILE fill:#a6e3a1,color:#11111b
⚠️
El feed no ejecuta tus islas

Un error frecuente es esperar que el content del feed muestre tus componentes interactivos. No lo hará: un lector RSS renderiza HTML plano, sin hidratar islas ni cargar scripts. Lo que incrustes debe ser HTML autosuficiente. Por eso el cuerpo se compila a un string y se sanea antes de meterlo, y por eso muchas veces basta con la description —un extracto— y dejar el artículo completo en el sitio.

El RSS es la web admitiendo que el contenido son datos

Antes de que hubiera una palabra para ello, el RSS ya era la web tratando su contenido como datos y no como páginas. Un feed es un sitio reconociendo en público que lo que publica no es una maqueta cerrada sino una corriente de registros que cualquiera puede consumir, transformar y volver a mostrar en un lector que el autor jamás controló. Esa idea —desacoplar el contenido de su presentación hasta el punto de regalar los datos en bruto— es la misma que sostiene todo este nivel, solo que el RSS la llevó al extremo hace dos décadas: aquí tienes mis artículos como XML, léelos donde quieras, con la tipografía que quieras, en la app que quieras, sin pasar por mi diseño. Cuando escribes un endpoint rss.xml no estás añadiendo una función más al sitio; estás declarando que tu contenido es una fuente abierta y que una URL puede devolver la representación que su consumidor necesite, no la que a ti te conviene enseñar. La colección que alimenta tus páginas alimenta también el feed sin duplicarse porque nunca fue un montón de páginas: fue siempre una base de datos de la que el HTML es apenas una lectura, y el XML del feed, otra. Interiorizar eso cambia la pregunta que te haces al construir: dejas de preguntarte cómo se ve mi sitio y empiezas a preguntarte qué representaciones de mis datos merece cada consumidor, que es la pregunta que gobierna las cinco lecciones de este nivel.

⚔️ Sindica tu colección
  1. Instala @astrojs/rss, fija site en astro.config y crea src/pages/rss.xml.js con un feed de items vacío; visita /rss.xml.
  2. Rellena items desde getCollection('blog') excluyendo borradores, mapeando title, pubDate, description y link.
  3. Añade categories desde las etiquetas y un customData con el idioma; valida el XML resultante en un lector RSS.
  4. Coloca un <link rel="alternate" type="application/rss+xml"> en la cabecera para que los navegadores descubran el feed, y razona por qué el feed no es una copia del contenido sino otra proyección de la misma colección.