Consultar y renderizar: getCollection, getEntry y render
Convertir una colección tipada en páginas: cargar entradas con getCollection y getEntry, filtrarlas y ordenarlas con datos ya seguros, renderizar su cuerpo con render y el componente Content, y derivar rutas del contenido con getStaticPaths.
Con el contenido descubierto, validado y tipado, la última pieza es consumirlo. Y aquí las content collections revelan su carácter: se consultan como una pequeña base de datos. Pides una colección entera o una entrada suelta, filtras y ordenas con datos que ya tienen tipo, y cuando necesitas el cuerpo del Markdown renderizado a HTML, lo pides aparte. Contenido como datos primero, presentación después: esa separación es la que hace que una misma colección alimente una portada, una página de detalle y un feed sin repetir el trabajo.
- Cargar colecciones y entradas con
getCollectionygetEntry. - Filtrar y ordenar entradas aprovechando que su
dataestá tipado. - Renderizar el cuerpo de una entrada con
rendery el componente<Content />. - Derivar rutas del contenido combinando la consulta con
getStaticPaths.
getCollection y getEntry
Dos funciones de astro:content cubren la consulta. getCollection devuelve todas las entradas de una colección como un array; getEntry devuelve una sola, identificada por su id. Ambas son asíncronas, así que se esperan con await en el frontmatter del componente.
Que sean asíncronas no implica que consulten nada en tiempo de ejecución: en un sitio estático, estas llamadas se resuelven durante el build contra el almacén ya cargado, y su coste desaparece del navegador. El await está ahí porque la API es uniforme —la misma que usarías si el loader trajera datos remotos—, no porque haya una espera real en producción. Escribes código asíncrono y entregas HTML estático.
import { getCollection, getEntry } from 'astro:content';
const posts = await getCollection('blog');
const uno = await getEntry('blog', 'mi-post');
Cada entrada es un objeto con una forma fija: un id estable, el data ya validado y tipado según tu esquema, el body con el texto crudo del Markdown y el nombre de la collection. Como data conoce su tipo, post.data.title es string y post.data.pubDate es Date, con autocompletado y con el compilador vigilando cada acceso. La consulta no devuelve texto opaco, sino registros con contrato.
getEntry merece un cuidado especial: si le pasas un id que no existe, devuelve undefined, no lanza una excepción. Esa elección deja en tus manos decidir qué hacer con lo ausente —redirigir, mostrar un 404, usar un valor de reserva—, pero te obliga a comprobarlo. En una ruta que recibe el id desde la URL, ese chequeo es la diferencia entre una página de error clara y un fallo turbio al leer data de algo que no está.
Filtrar y ordenar
Al ser un array de datos tipados, una colección se manipula con las herramientas de siempre de JavaScript. getCollection acepta además un segundo argumento, una función de filtro que se aplica durante la carga: útil para descartar borradores sin traerlos siquiera al array.
const publicados = await getCollection('blog', ({ data }) => !data.draft);
const ordenados = publicados.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
Ordenar por fecha descendente, agrupar por etiqueta, quedarte con los tres más recientes: todo es código normal sobre un array, con la ventaja de que el tipado impide errores tontos —restar dos fechas está permitido, restar dos títulos no compila—.
Ese detalle —que el tipo te impida una operación absurda— es el tipado ganándose el sueldo. En una lectura a mano, ordenar por una fecha que en realidad es una cadena produce un orden alfabético traicionero que parece funcionar hasta que llega diciembre y “10” se coloca antes que “9”. Con la colección, pubDate es un Date, restarlo es legítimo y el orden sale correcto por construcción; el error ni siquiera es posible. La colección se comporta como una tabla en memoria que consultas con el lenguaje que ya sabes.
Conviene decidir dónde filtras. La función que pasas a getCollection se evalúa durante la carga y es ideal para exclusiones amplias —descartar borradores— porque esas entradas ni siquiera llegan al array. El filter, el sort o el slice posteriores operan sobre lo ya cargado y son el lugar natural para las vistas concretas: los cinco últimos, los de una etiqueta, los de un año. Una criba general en la carga; los recortes de cada página, después.
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<ul>
{posts.map((post) => (
<li><a href={`/blog/${post.id}`}>{post.data.title}</a></li>
))}
</ul>
render: del cuerpo al HTML
El data te da los metadatos, pero el cuerpo del Markdown —los párrafos, los encabezados, el código— hay que compilarlo a HTML para mostrarlo. Esa es la tarea de render, una función que importas de astro:content, a la que pasas la entrada y que te devuelve un componente Content listo para colocar en la plantilla.
---
import { getEntry, render } from 'astro:content';
const post = await getEntry('blog', 'mi-post');
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
render separa deliberadamente el dato de su representación: mientras no lo llamas, la entrada es puro dato consultable; cuando lo llamas, obtienes su HTML. Junto al Content recibes headings, la lista de encabezados del documento, con la que se construye una tabla de contenidos sin analizar el texto a mano.
El Content que devuelve render es un componente de Astro como cualquier otro: lo colocas en la plantilla, hereda los estilos de su contexto y respeta los componentes que hayas mapeado para el Markdown. Por debajo, render compila el body crudo de la entrada al HTML final, un trabajo que solo se hace cuando lo pides. Por eso una página índice, que lista títulos y fechas pero no cuerpos, no paga el coste de renderizar cien documentos que nadie va a leer en esa vista.
Distingue siempre lo barato de lo caro. Consultar data es leer un objeto ya cargado; renderizar el cuerpo compila Markdown. Una portada eficiente consulta muchos data y no renderiza ninguno; una página de detalle consulta uno y lo renderiza. Cargar la colección para listar y reservar render para la vista de detalle mantiene los builds rápidos aunque el blog crezca a miles de entradas.
En la Content Layer, render es una función independiente que importas y a la que pasas la entrada: render(post). Si vienes de guías antiguas verás la forma de método, post.render(), hoy en desuso. El cambio no es cosmético: separar render de la entrada deja claro que renderizar es una operación que se elige, no un adorno que la entrada arrastra siempre consigo.
De la colección a las rutas
El patrón que lo une todo es generar una página por entrada. En una ruta dinámica como src/pages/blog/[slug].astro, la función getStaticPaths recorre la colección y produce un camino por cada post, pasando la entrada entera como prop.
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<Content />
flowchart LR GC[getCollection blog] --> FIL[filtrar borradores] FIL --> ORD[ordenar por fecha] ORD --> MAP[un path por entrada] MAP --> RND[render de la entrada] RND --> CNT[Content a HTML] style GC fill:#89b4fa,color:#11111b style CNT fill:#a6e3a1,color:#11111b
Cada id se convierte en un segmento de URL, y cada entrada, en una página completa. La misma colección que aquí genera las páginas de detalle puede, en otra ruta, alimentar el índice del blog, y en un endpoint, un feed RSS. Una fuente, muchas vistas: el contenido se consulta una vez y se presenta de tantas formas como haga falta.
Ese patrón —getStaticPaths que recorre una colección y emite una ruta por entrada— es el puente entre las dos mitades del framework: el contenido tipado de este nivel y el enrutado por ficheros que ya conoces. Los datos deciden cuántas páginas existen; el fichero [slug].astro decide cómo es cada una. Añadir un post es crear un Markdown, y el sitio crece solo, sin tocar rutas ni registros.
El mismo patrón se estira sin romperse. Para paginar, getStaticPaths puede emitir una ruta por página de resultados en vez de por entrada; para un sitio en servidor, la consulta ocurre por petición en lugar de en el build, con el mismo código. Cambia cuándo y cuántas veces se ejecuta la consulta, no cómo se escribe. Aprendido una vez, el gesto de pedir a la colección y decidir la presentación se reutiliza en cada modo de renderizado.
Lo que de verdad desbloquean getCollection y render juntos es una separación que arrastra siglos de buen diseño: distinguir los datos de su presentación. Sin collections, un post era un fichero que se leía y se pintaba de una vez, con los metadatos y el HTML enredados en el mismo gesto. Con collections, el post es primero un registro consultable —data— y solo cuando lo decides se convierte en HTML —render—. Esa frontera entre “el dato” y “su forma visible” es la misma que separa un modelo de una vista, una tabla de un informe, el contenido de la maqueta. Y tiene una consecuencia que cambia cómo diseñas un sitio entero: si el contenido es una capa de datos que puedes consultar, filtrar y ordenar con independencia de cómo se muestre, entonces una sola fuente puede proyectarse en cuantas vistas necesites sin duplicarse. El mismo array de posts genera las páginas de detalle, la portada con los últimos cinco, el archivo por año, la nube de etiquetas, el feed RSS y el sitemap. Ninguna de esas vistas posee el contenido; todas lo derivan. Cuando interiorizas que getCollection no te devuelve páginas sino datos —y que las páginas son solo una de sus proyecciones posibles— dejas de pensar en “escribir posts” y empiezas a pensar en “tener una base de contenido” de la que el sitio visible es apenas una lectura. Ese giro, del documento a la consulta, es lo que permite que un proyecto crezca de diez a mil entradas sin que la forma de construirlo cambie: siempre es pedir datos a la colección y decidir cómo mostrarlos.
- Usa
getCollection('blog')en una página índice y pinta una lista con eltitley lapubDatede cada entrada. - Filtra los borradores con la función de filtro de
getCollectiony ordena el resto por fecha descendente. - Crea
src/pages/blog/[slug].astro, genera sus rutas congetStaticPathsa partir delid, y renderiza cada post conrendery<Content />. - Reutiliza la misma colección para mostrar en la portada solo los tres posts más recientes, y razona por qué no has tenido que duplicar ningún dato.