En este artículo
Si alguna vez armaste un blog o un sitio de contenidos con archivos Markdown a mano en cualquier framework, seguro conocés la sensación de terror cuando una fecha mal formateada o un tag mal escrito en el frontmatter te tira el build a la basura en pleno despliegue.
Durante años, la gestión de contenido local en la web estática fue un terreno sin ley: leías carpetas con fs, parseabas texto con librerías tipo gray-matter y rezabas para que nadie hubiera puesto autor en vez de author.
Astro cambió esa dinámica por completo cuando introdujo Content Collections, y con sus últimas evoluciones transformó una simple herramienta de validación de Markdown en una capa de datos universal (Content Layer API) que hoy compite de frente con GraphQL y los SDKs tradicionales de headless CMS.
Acá te cuento qué son exactamente, cuándo te conviene meterlas en tu proyecto, cómo evolucionaron hasta hoy y hacia dónde va el ecosistema.
¿Qué son exactamente las Content Collections?
En criollo: una Content Collection es una carpeta o fuente de datos a la que Astro le aplica un esquema estricto y tipado de TypeScript en tiempo de compilación.
En lugar de tratar a tus archivos .md, .mdx o .json como texto plano suelto, Astro los procesa a través de un esquema definido con Zod. Si un campo es obligatorio, si una fecha tiene formato inválido o si un autor no existe en tu base de datos, el compilador te avisa en tu terminal y en tu editor antes de que el error llegue al navegador.
Además, te da autocompletado nativo. Al escribir post.data. en tu componente .astro, el editor ya sabe exactamente qué propiedades existen, si son strings, arrays o números.
La evolución: De simples carpetas locales a la Content Layer API
Para entender el poder que tienen hoy, vale la pena mirar el camino que hicieron:
1. El inicio (Astro 2.x – 3.x): El reinado de src/content/
Al principio, las colecciones eran estrictamente de archivos locales. Creabas una carpeta dentro de src/content/ (por ejemplo src/content/blog/), metías tus markdowns, y en src/content/config.ts definías el schema con Zod. Era genial, pero tenía una limitación clara: si tus datos venían de una API externa, un Notion o una base de datos SQL, quedabas afuera de este sistema y tenías que inventar tus propios fetchs manuales.
2. El presente: La Content Layer API y los Loaders
La arquitectura dio un salto gigante. Ya no estás atado a la estructura rígida de carpetas. Ahora la configuración vive en src/content.config.ts y se apoya en el concepto de Loaders:
- Carga local o remota: Podés usar el loader nativo
glob()para leer markdowns locales, pero también podés usar el loaderfile()para JSONs/YAMLs, o conectar loaders de terceros para traer contenido directo desde Contentful, Strapi, Sanity, feeds RSS o bases de datos SQLite. - Almacenamiento interno ultra-optimizado: Astro guarda los datos procesados en un almacén local en caché. Esto acelera drásticamente los tiempos de build porque no recompila ni vuelve a pedir datos a APIs externas si no cambiaron.
- Separación de responsabilidades: La lógica de dónde vienen los datos (API, archivo local, base de datos) queda desacoplada de cómo los consultas en tus páginas con
getCollection()ogetEntry().
Cómo se ve hoy en código
La estructura actual es ridículamente limpia y expresiva:
TypeScript
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
// Cargamos los archivos usando el loader moderno
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/data/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.date(),
description: z.string(),
author: z.string().default('Anónimo'),
tags: z.array(z.string()),
draft: z.boolean().optional(),
}),
});
export const collections = { blog };
Y para consumirlo en cualquier página o componente:
Code snippet
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map(post => ({
params: { slug: post.id },
props: { post },
}));
}
const { post } = Astro.props;
// Renderizamos el markdown de forma atómica
const { Content, headings } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<time>{post.data.pubDate.toLocaleDateString()}</time>
<Content />
</article>
¿Cuándo te conviene usarlas (y cuándo es mejor evitar complejidad)?
Usalas sin dudar si:
- Tenés contenido editorial estructurado: Blogs, documentación técnica, changelogs, portfolios, directorios o bases de conocimiento.
- Trabajás con un equipo de redactores/diseñadores: Si alguien rompe un campo del frontmatter, el build frena inmediatamente y te marca el archivo y la línea exacta del error.
- Consumís datos de múltiples fuentes: Querés unificar en una sola API tipada (
getCollection) datos que vienen parte de archivos Markdown locales y parte de una API headless.
Mejor resolverlo simple si:
- Son datos de un solo uso en una sola página: Si solo necesitás mostrar un texto estático en una landing page de contacto, crear una colección con schema es sobreingeniería innecesaria; dejalo como texto plano en el componente.
- Datos altamente transaccionales en tiempo real: Si estás construyendo un dashboard con métricas que cambian segundo a segundo o datos dependientes de la sesión de un usuario autenticado, esto no va en Content Collections; va por Server-Side Rendering (SSR) tradicional con llamadas a API en tiempo de ejecución.
Lo que SÍ y lo que NO con Content Collections
Lo que SÍ debes hacer
- Aprovechar las transformaciones de Zod: Podés usar
.transform()en tu schema para formatear fechas automáticamente, sanitizar textos o calcular tiempos de lectura en el paso de validación. - Filtrar directo en
getCollection: Pasale una función de filtrado al método (ej. ignorar borradores con({ data }) => !data.draft) para no procesar entradas innecesarias en memoria. - Modularizar schemas compartidos: Si varias colecciones usan campos comunes (como SEO o autorías), exportá sub-schemas de Zod y reutilizalos con
.extend().
Lo que NO debes hacer
- Hacer queries pesadas adentro del ciclo de renderizado: Obtené tus colecciones siempre en el frontmatter (
---) del componente, nunca mezcles llamadas de datos en componentes cliente hidratados si ya podés resolverlo en el servidor. - Ignorar los tipos inferidos: No redeclares interfaces de TypeScript a mano. Usá
CollectionEntry<'blog'>para tipar props de componentes hijos con total fidelidad.
¿Hacia dónde va el ecosistema?
La dirección que tomó Astro con la capa de contenido apunta a resolver dos grandes problemas de la web moderna:
- Builds incrementales ultrarrápidos: El sistema de caché del Content Layer está diseñado para que proyectos con miles de entradas no tengan que regenerar todo el sitio desde cero si solo cambió una línea de un artículo.
- Ecosistema de loaders oficiales y comunitarios: La tendencia es que ya no tengamos que escribir clientes de API para CMSs populares; simplemente importás el loader oficial (de WordPress REST, Notion, Supabase o MicroCMS), le pasás tu API Key y Astro se encarga de la paginación, el guardado en caché y la validación de tipos en segundo plano.
Content Collections pasó de ser “un validador lindo de Markdown” a convertirse en el estándar de facto para estructurar datos en el desarrollo web orientado a contenido. Una vez que te acostumbrás a trabajar con esquemas estrictos y autocompletado en tus posts, volver al parsing manual de archivos se siente como programar a ciegas.