La Content Layer API: qué es y por qué
El salto del modelo antiguo de content collections —atado a la carpeta mágica src/content— a la Content Layer API de Astro 7: colecciones definidas por un loader que nombra su fuente, un store normalizado en tiempo de build y una escalabilidad que leer una carpeta nunca tuvo.
Detrás de cada content collection late una maquinaria que rara vez se nombra: la Content Layer API. Es la capa que responde a una sola pregunta —de dónde salen los datos y dónde quedan— y la responde separando dos piezas que el modelo antiguo mantenía fundidas. El loader dice de dónde viene el contenido; el store es el almacén donde ese contenido queda, ya normalizado, una vez cargado. Desde Astro 5, y como estándar único en Astro 7, una colección deja de ser una carpeta que el framework vigila y pasa a ser un contrato sobre una fuente cualquiera cuyo resultado se materializa en un almacén construido durante el build.
- Contrastar el modelo antiguo de collections, atado a
src/content, con la Content Layer API basada en loaders. - Entender el
storecomo el almacén normalizado donde el loader deposita las entradas en tiempo de build. - Ver por qué desacoplar la fuente del consumo permite escalar a miles de entradas y a fuentes remotas.
- Situar
glob,filey los loaders propios como implementaciones de un mismo contrato.
Dos modelos, una misma forma de consumir
Para medir el cambio conviene recordar el modelo que sustituye. Hasta Astro 5, una colección era, literalmente, una carpeta. El contenido tenía que vivir en src/content/<coleccion>, el fichero de configuración se llamaba src/content/config.ts, y cada colección se declaraba con un type que solo admitía dos valores: content para Markdown y data para JSON o YAML. Astro recorría esa carpeta por convención, y esa convención era a la vez su comodidad y su cárcel: el contenido no podía estar en otro sitio ni venir de otra parte que no fuera el disco local.
// Modelo antiguo (Astro < 5): la carpeta manda
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content', // 'content' | 'data'
schema: z.object({ title: z.string(), pubDate: z.date() }),
});
export const collections = { blog };
La Content Layer invierte ese planteamiento. En lugar de un type que elige entre dos formatos locales, cada colección recibe un loader: una pieza que sabe traer contenido de donde sea. El fichero se muda a src/content.config.ts —fuera de la carpeta content, porque ya no depende de ella— y el type desaparece, absorbido por el loader. Lo esencial es que el loader es un contrato abierto: glob y file son los dos que Astro trae de fábrica, pero cualquiera puede escribir el suyo, y todos rellenan el mismo almacén.
// Content Layer (Astro 7): el loader manda
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({ title: z.string(), pubDate: z.date() }),
});
export const collections = { blog };
Lo que no cambia —y esto es deliberado— es cómo consumes la colección. Sigues llamando a getCollection('blog') y a getEntry, sigues recibiendo entradas tipadas contra tu esquema. La API de consumo es estable a propósito, porque el objetivo de toda la reforma no es que aprendas nuevas funciones para leer datos, sino que la fuente de esos datos pueda cambiar sin que tu código se entere. Debajo, en cambio, todo es distinto: la carpeta mágica se ha convertido en una abstracción sobre orígenes.
El consumo es estable, pero no idéntico al de las collections antiguas, y conviene anotar dos ajustes que la migración trajo. El método entry.render() dio paso a una función render que importas de astro:content y a la que pasas la entrada; y el antiguo campo slug, que solo tenían las collections de tipo content, se unificó bajo el id que ahora llevan todas. Son cambios pequeños en la superficie que reflejan la unificación de fondo: ya no hay dos clases de entrada —contenido y datos— sino una sola forma común, con id para todas y cuerpo solo para las que lo tengan.
import { getEntry, render } from 'astro:content';
const entrada = await getEntry('blog', 'mi-post');
const { Content } = await render(entrada); // antes: entrada.render()
En Astro 7 esto no es una opción entre dos modelos: el antiguo se retiró por completo y type: 'content' ya no existe. Toda colección pasa por un loader, y los proyectos que venían de versiones anteriores tuvieron que migrar su config.ts a content.config.ts y su type a un glob loader equivalente. Lo que empezó como una alternativa opcional en Astro 5 es hoy el único camino, y esa consolidación es la que justifica dedicarle un nivel entero.
El fichero pasó de src/content/config.ts a src/content.config.ts, un peldaño por encima de la carpeta content. No es un capricho: subraya con la ubicación que las colecciones ya no dependen de esa carpeta. Puedes conservar tu Markdown en src/content/blog si quieres, pero ahora es una decisión tuya expresada en el base del loader, no una imposición del framework.
El store: contenido normalizado en el build
Toda la Content Layer gira alrededor de una estructura que el modelo antiguo no exponía: el store, un almacén de datos —un DataStore— donde los loaders depositan sus entradas. Cuando arranca el servidor de desarrollo o se lanza un build, Astro ejecuta el loader de cada colección; el loader hace su trabajo —leer ficheros, llamar a una API— y escribe el resultado en el store como un conjunto de entradas planas. Solo después, a partir de ese store ya poblado, Astro genera los tipos y sirve getCollection.
Cada entrada del store tiene una forma canónica, independiente de su origen: un id que la identifica, un data con los campos ya validados y —si el contenido lo tiene— un body con el texto sin renderizar y un rendered con su HTML. Da igual que la entrada venga de un Markdown local o de un JSON remoto: al llegar al store, ambas comparten la misma estructura. Esa normalización es lo que permite que una sola API de consumo sirva para cualquier fuente.
flowchart TD OLD[modelo antiguo] --> MAGIC[carpeta src content obligatoria] MAGIC --> LOCAL[solo ficheros locales] NEW[content layer] --> LOADER[loader que nombra la fuente] LOADER --> SRC[ficheros API CMS o base de datos] LOADER --> STORE[store con entradas normalizadas] STORE --> TYPES[tipos generados] TYPES --> API[getCollection y getEntry] style OLD fill:#f38ba8,color:#11111b style LOCAL fill:#f38ba8,color:#11111b style STORE fill:#a6e3a1,color:#11111b style API fill:#a6e3a1,color:#11111b
El store, además, no es efímero. Astro lo persiste entre builds en la carpeta .astro, en un fichero de datos que sobrevive de una compilación a la siguiente. Esa persistencia es la semilla del caché incremental que verás al final del nivel: un loader bien escrito puede consultar el store previo, comparar lo que ya tenía con lo que la fuente ofrece ahora y actualizar solo lo que cambió, en vez de reconstruirlo todo. El modelo antiguo releía la carpeta entera en cada build; el store abre la puerta a no repetir trabajo.
Conviene ver el store como lo que es: una pequeña base de datos de build-time. No es una caché opaca ni un detalle de implementación que puedas ignorar, sino la pieza central sobre la que los loaders operan con un puñado de métodos —store.set, store.get, store.entries, store.clear—. Cuando en las próximas lecciones escribas tu propio loader, tu trabajo consistirá, en esencia, en llenar bien ese store; todo lo demás —tipos, validación, consulta— Astro lo construye encima de él.
Ese paso de generar los tipos tiene nombre propio: astro sync, que el framework dispara solo al arrancar y cada vez que cambias la configuración. Recorre el store ya poblado y emite las definiciones de TypeScript que hacen que data tenga forma en cada colección. Por eso el autocompletado de getCollection('blog') conoce los campos sin que instales nada: se derivan del contenido que el loader depositó y del esquema que lo validó. Si alguna vez el editor parece no ver un campo que sí declaraste, forzar astro sync suele realinear los tipos con la realidad.
Conviene precisar que el store no es un único saco global, sino que está particionado por colección: cada una tiene su propio espacio de entradas y de claves. Esa partición es la que permite que dos colecciones compartan un mismo id sin pisarse, y que Astro recargue una colección sin tocar las demás. El store es, más que una base de datos monolítica, un conjunto de pequeñas tablas independientes, una por cada nombre que declaraste en collections.
La carpeta .astro —donde viven el store persistido y los tipos generados— es un artefacto reconstruible, no una fuente. Debe ir en tu .gitignore: se regenera entera a partir de content.config.ts y de tus fuentes cada vez que hace falta. Versionarla solo añade ruido y conflictos, y borrarla nunca pierde información, porque todo lo que contiene se puede volver a derivar.
La fuente, desacoplada del consumo
El verdadero motivo de este rediseño es la escala, entendida en dos ejes. El primero es el volumen: un sitio con decenas de miles de entradas no puede permitirse releer y reparsear todo su contenido en cada build. Con el store persistente y los loaders incrementales, el trabajo se vuelve proporcional a lo que cambia, no a lo que existe. Un cambio en tres ficheros no debería costar lo mismo que arrancar de cero, y con la Content Layer no lo cuesta.
El segundo eje es el origen. Atado a src/content, el modelo antiguo solo sabía de ficheros locales; traer datos de un CMS o de una API obligaba a rodeos que quedaban fuera del sistema de collections, sin sus tipos ni su validación. El loader disuelve esa frontera: como es una simple pieza que rellena el store, da igual que lo haga leyendo un disco o llamando a un servicio remoto. El contenido de terceros entra por la misma puerta que el local y sale igual de tipado.
Hay un tercer beneficio, más silencioso, que se deriva de los dos anteriores: la homogeneidad. Cuando todo el contenido —local y remoto, mucho y poco— entra por el mismo contrato y sale con la misma forma, tu código deja de tratar cada fuente como un caso especial. Un componente que pinta la tarjeta de un post no necesita saber si ese post vino de un Markdown o de un CMS: recibe una entrada tipada y la pinta. Esa uniformidad es la que permite que un sitio crezca sin multiplicar las ramas de código que lo sostienen.
Loader
La pieza que trae el contenido de su fuente —local o remota— y lo escribe en el store. Sustituye al viejo type fijo.
Store
El almacen normalizado donde viven las entradas en el build. Persiste en .astro entre compilaciones.
Entrada
La unidad del store: un id, un data validado y, si aplica, body y contenido renderizado.
API estable
getCollection y getEntry no cambian, venga el dato de donde venga. El consumo es indiferente al origen.
Este desacople tiene un nombre en arquitectura: es una capa de indirección entre la fuente y el consumidor. Y como toda indirección bien puesta, su valor no está en lo que te deja hacer hoy, sino en lo que te deja cambiar mañana sin romper nada. Empiezas con un blog de Markdown local; dentro de un año lo migras a un CMS porque el equipo de contenido creció. Con el modelo antiguo, esa migración tocaba cada plantilla; con la Content Layer, cambia un loader en content.config.ts y nada más se entera.
La prueba de fuego de que el desacople funciona es un experimento mental: para mover tu blog de Markdown local a un CMS, ¿cuántos ficheros tocas? En el modelo antiguo, todos los que leían la carpeta. Con la Content Layer, uno solo —content.config.ts, donde sustituyes el glob loader por el del CMS— siempre que el esquema siga describiendo la misma forma. Si tu migración toca más de ese fichero, es señal de que alguna plantilla asumió algo del origen que no debía saber.
Un desacople que también cuesta
Sería deshonesto vender la Content Layer como puro beneficio. Toda indirección tiene un precio, y el suyo se paga en dos monedas. La primera es conceptual: hay más piezas que entender —loader, store, meta, digest— que en el viejo “pon tus ficheros en esta carpeta”. Para un sitio diminuto, esa carga cognitiva puede no compensar, y no pasa nada por reconocerlo.
La segunda moneda es la del caché. Un store que persiste entre builds es una fuente de velocidad, pero también de estados sutiles: un caché desactualizado puede servir datos viejos, y depurar “por qué veo contenido que ya borré” exige saber que existe una capa de memoria en .astro que a veces hay que limpiar. El modelo antiguo, que releía todo cada vez, era más lento pero más ingenuo: no tenía una memoria que pudiera mentir.
Nada de esto invalida el diseño; solo lo sitúa. La Content Layer optimiza para el sitio que crece —más contenido, más fuentes, más gente— y acepta a cambio algo más de maquinaria. Es el trueque habitual de la ingeniería: pagas algo de complejidad hoy para comprar escala mañana. Conocer ese precio es justo lo que te permite decidir con criterio cuándo merece la pena y cuándo un simple glob sobre una carpeta local es más que suficiente.
El cambio de fondo de la Content Layer API no es técnico sino conceptual, y se resume en una frase: el contenido dejó de ser un lugar para convertirse en un contrato. En el modelo antiguo, “una colección” era una respuesta a la pregunta dónde —en src/content, en esa carpeta, con ese nombre—. En la Content Layer, “una colección” responde a qué forma y desde qué fuente, y el dónde se vuelve un detalle que el loader encapsula. Este giro —de identificar las cosas por su ubicación a identificarlas por su contrato— es uno de los movimientos recurrentes de la buena ingeniería: es el mismo salto que va de acceder a un fichero por su ruta a pedirlo a un repositorio, de leer un puerto concreto a hablar con un driver, de depender de una implementación a depender de una interfaz. Lo que se gana no es solo flexibilidad de origen, sino una separación de responsabilidades que aclara el diseño entero: por un lado, la máquina que trae los datos —el loader, cambiante, acoplada a una fuente concreta—; por otro, el mundo que los consume —tus plantillas, estables, que solo conocen entradas tipadas—. Entre ambos, el store hace de frontera y de memoria. Cuando interiorizas esta separación dejas de preguntarte “dónde pongo mis ficheros” y empiezas a preguntarte “qué fuentes alimentan mi sitio y qué forma les impongo al entrar”, que es una pregunta de arquitecto, no de administrador de carpetas. El resto del nivel es, en el fondo, aprender a escribir el lado izquierdo de ese contrato —los loaders— sabiendo que el lado derecho —el consumo— ya está resuelto y es inamovible.
- Abre un proyecto con
src/content.config.tsy localiza, para una colección, suloadery suschema; nombra qué aporta cada uno. - Ejecuta un build y busca la carpeta
.astro; identifica el fichero donde Astro persiste el store de datos. - Explica, en dos frases, por qué
getCollection('blog')no necesita cambiar aunque mañana el blog pase de Markdown local a una API remota. - Enumera las dos dimensiones de escala —volumen de entradas y variedad de orígenes— y razona cuál de las dos te aprieta más en tu proyecto actual.