content.config.ts y el glob loader
El fichero donde se declaran todas las colecciones: defineCollection, la exportación collections y el glob loader con sus opciones pattern y base. Cómo Astro descubre el contenido, deriva el id de cada entrada y genera los tipos en Astro 7.
content.config.ts: el registro de colecciones
El fichero src/content.config.ts vive en la raíz de src, junto a pages y components, y Astro lo busca por nombre. Su trabajo es exportar un objeto collections cuyas claves son los nombres de tus colecciones y cuyos valores son las definiciones. Ese objeto es la única fuente de verdad sobre qué colecciones conoce el proyecto.
// src/content.config.tsimport { 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 };La clave con la que registras la colección —blog— es su identidad para siempre: es la cadena que pasarás a getCollection y a getEntry. Importas defineCollection y z del módulo virtual astro:content, y los loaders de astro/loaders. Un solo fichero declara todo el contenido del sitio, por muchas colecciones que llegue a tener.
Las importaciones de la cabecera revelan la arquitectura en miniatura. De astro:content vienen las piezas que describen —defineCollection, z y, más adelante, reference—; de astro/loaders vienen las que traen —glob, file—. Describir y traer son las dos mitades de toda colección, y este fichero las junta: cada entrada de collections es una fuente conectada a una forma.
El objeto collections es, además, una lista blanca. Solo las claves que exportas ahí existen como colecciones: una carpeta llena de Markdown que no aparezca en este registro es invisible para getCollection, y a la inversa, no puedes consultar una colección que no hayas declarado. Esa explicitud es deliberada —nada se vuelve contenido por accidente— y convierte a content.config.ts en el índice fiable de todo lo que tu sitio considera datos.
defineCollection y el loader
defineCollection recibe un objeto con dos piezas: un loader, que dice de dónde sale el contenido, y un schema, que dice qué forma tiene. El loader es la gran novedad conceptual de la Content Layer: desacopla la colección de cualquier carpeta concreta y la convierte en un contrato sobre una fuente, sea cual sea esa fuente.
Conviene notar que el schema es, técnicamente, opcional. Una colección sin esquema sigue funcionando —Astro infiere lo que puede—, pero renuncias justo a lo que hace valiosas a las collections: la validación y el tipado fuerte. En la práctica, declarar el esquema es la norma, y omitirlo, una excepción para exploraciones rápidas. Las dos piezas se complementan: el loader dice de dónde, el schema dice qué forma.
Esta dualidad se refleja incluso en los nombres. Un loader es un verbo —trae, carga—; un schema es un sustantivo —una forma, una estructura—. Toda colección es la unión de una acción y una descripción: hacer llegar unos datos y decir cómo deben ser. Perder de vista cualquiera de las dos mitades produce colecciones frágiles: sin loader no hay contenido, y sin schema no hay garantías sobre él.
Astro trae dos loaders integrados, y cubren la mayoría de los casos locales:
Más allá de estos dos, el mismo hueco admite loaders de terceros o propios: uno que traiga entradas desde un CMS, desde una API remota o desde una base de datos. Lo notable es que el código que consume la colección —getCollection, getEntry— no cambia ni una línea según el origen. Esa indiferencia ante la fuente es justo lo que compra la abstracción del loader.
El file loader ilustra bien esa indiferencia. En lugar de un fichero por entrada, toma uno solo que contiene una lista y produce una entrada por cada elemento. Es el formato natural para datos que editas juntos —autores, países, precios— y que no merecen un Markdown cada uno.
import { defineCollection, z } from 'astro:content';import { file } from 'astro/loaders';const autores = defineCollection({ loader: file('src/data/autores.json'), schema: z.object({ id: z.string(), nombre: z.string(), }),});El consumo será idéntico al del blog: getCollection('autores') devuelve entradas tipadas, sin que a la plantilla le importe que detrás haya un JSON y no una carpeta de Markdown. Ese es el contrato del loader cumpliéndose. Y como el contrato no depende del origen, migrar es barato: empezar con glob sobre ficheros locales y, cuando el equipo de contenido crezca, sustituir ese loader por uno de un CMS es cambiar una línea de content.config.ts; ni las consultas ni las plantillas se enteran.
El glob loader: pattern y base
El glob loader se configura con dos opciones. base es el directorio desde el que se resuelve la búsqueda —una ruta relativa a la raíz del proyecto—. pattern es el patrón glob que selecciona los ficheros dentro de ese base.
loader: glob({ pattern: '**/*.md', base: './src/content/blog' })El patrón **/*.md significa “todos los .md, a cualquier profundidad”. Puedes afinarlo: *.md se limita al nivel superior, **/*.{md,mdx} admite ambas extensiones, y un array de patrones combina reglas, incluida la exclusión con ! para descartar borradores o plantillas. El pattern es tu control fino sobre qué ficheros entran en la colección y cuáles quedan fuera de ella.
Esa expresividad resuelve casos reales sin ceremonia. Un patrón como ['**/*.md', '!**/_*.md'] incluye todo el Markdown salvo el que empieza por guion bajo, un convenio cómodo para marcar borradores. Un base distinto por colección mantiene separados los posts de las páginas aunque ambos sean Markdown. El loader no te impone una única disposición: se amolda a la que tenga sentido para tu contenido.
De la ruta de cada fichero, Astro deriva su id: toma la ruta relativa al base, le quita la extensión y la normaliza. Así src/content/blog/mi-post.md obtiene el id mi-post, y un fichero anidado en 2026/enero.md obtiene 2026/enero. Ese id es la llave estable con la que localizarás la entrada y, casi siempre, el slug de su URL.
Puedes intervenir en esa derivación cuando lo necesites: el glob loader admite una función generateId para forjar el id a tu gusto —a partir de un campo del frontmatter, por ejemplo, en lugar del nombre del fichero—. Pero el valor por defecto, la ruta relativa sin extensión, es tan buen predeterminado que rara vez lo tocarás. La regla implícita es sana y familiar: el sitio de un fichero decide su identidad, igual que en el enrutado por ficheros.
Cómo Astro descubre el contenido
Al arrancar el servidor de desarrollo o al compilar, Astro lee content.config.ts, ejecuta el loader de cada colección y guarda los resultados en un almacén interno de datos. A partir de ese almacén genera los tipos de TypeScript de cada colección —el proceso que dispara astro sync— de modo que getCollection('blog') conozca exactamente la forma de data.
En desarrollo, Astro vigila los ficheros del base: si añades, editas o borras un Markdown, la colección se recarga sin reiniciar. Este ciclo —descubrir, cargar, validar, tipar— ocurre antes de renderizar ninguna página, y por eso el contenido llega a tus plantillas ya comprobado. El registro que escribiste en un fichero se convierte, en cada build, en un conjunto de datos vivo y tipado.
Ese almacén y esos tipos se materializan en una carpeta generada, .astro, que no editas y que conviene ignorar en el control de versiones. Es un artefacto, no una fuente: se reconstruye entero a partir de content.config.ts y de tus ficheros cada vez que hace falta. Si alguna vez el editor parece no conocer un campo que sí declaraste, forzar la sincronización con astro sync suele bastar para realinear los tipos con la realidad.
El resultado neto es que, para cuando tu primera página pide getCollection, el trabajo pesado ya ocurrió: los ficheros se leyeron, el esquema los validó y los tipos se emitieron. Consultar la colección es leer un almacén ya preparado, no lanzar una carga en caliente. Esa separación entre preparar los datos y consumirlos es la que mantiene rápidas las plantillas.
Y como content.config.ts es TypeScript de pleno derecho, nada te impide construir sus colecciones con lógica: derivar un base de una variable de entorno, generar varios esquemas a partir de una pieza común o compartir tipos con el resto del proyecto. El registro no es un formato de datos inerte, sino código que se ejecuta al arrancar; esa naturaleza es la que abre la puerta a loaders remotos y a configuraciones que se adaptan al entorno.