wandres.dev
CONTENT COLLECTIONS · glob loader y schema

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.

⏱ 14 min

Toda colección empieza en un único fichero: src/content.config.ts. Ahí declaras qué colecciones existen, de dónde sale su contenido y qué forma tienen. Es el registro central que Astro lee al arrancar para saber qué datos gestiona. Y la pieza que conecta ese registro con los ficheros reales es el loader: en Astro 7 el contenido ya no vive en una carpeta mágica, sino que se trae explícitamente desde una fuente que tú nombras.

🎯 Al terminar esta lección sabrás
  • Situar src/content.config.ts como el registro central de las colecciones.
  • Definir una colección con defineCollection y exponerla en collections.
  • Configurar el glob loader con sus opciones pattern y base.
  • Entender cómo Astro descubre el contenido y deriva el id de cada entrada.

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.ts
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 };

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 describendefineCollection, z y, más adelante, reference—; de astro/loaders vienen las que traenglob, 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.

📝
La ubicación importa

El fichero debe llamarse content.config.ts y estar en src, no dentro de src/content. Versiones antiguas lo situaban en src/content/config.ts; el modelo actual de la Content Layer lo saca a src/content.config.ts precisamente porque las colecciones ya no dependen de esa carpeta. Astro lo descubre por convención de nombre: si no aparece, no hay colecciones que consultar.

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:

📁

glob

Toma muchos ficheros y produce una entrada por cada uno. Ideal para un blog o unos documentos, donde cada Markdown es un post.

📄

file

Toma un único fichero de datos —un JSON o un YAML con un array— y produce una entrada por cada elemento. Ideal para catálogos y listas.

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.

💡
Una colección, una carpeta

El hábito más sano es dar a cada colección su propia carpeta bajo base y no mezclar dos colecciones en el mismo directorio. Así el pattern se mantiene simple, los id no colisionan y, al abrir el proyecto, la estructura de carpetas ya cuenta cuántas colecciones hay y qué contiene cada una. La disposición física se convierte en documentación.

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.

⚠️
base no es src/content por defecto

Con la Content Layer debes indicar base de forma explícita; no se asume src/content. Es liberador —tu contenido puede vivir donde quieras, incluso fuera de src— pero también significa que un base mal escrito produce una colección vacía sin ningún error ruidoso. Si getCollection te devuelve un array vacío, sospecha primero del base.

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.

flowchart TD
CFG[content.config.ts] --> DC[defineCollection]
DC --> LD[glob loader con pattern y base]
LD --> FS[ficheros descubiertos]
FS --> ID[id derivado de la ruta]
FS --> STORE[almacen de datos]
STORE --> TYPES[tipos generados]
TYPES --> API[getCollection y getEntry]
style CFG fill:#89b4fa,color:#11111b
style API fill:#a6e3a1,color:#11111b

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.

El loader invierte quién manda sobre el contenido

La aparente comodidad del glob loader esconde una inversión de control que define toda la Content Layer. En el modelo antiguo, el framework mandaba: había una carpeta sagrada, src/content, y tu contenido tenía que vivir allí, con las reglas que Astro imponía. El loader le da la vuelta a esa relación. Ahora tú declaras la fuente —estos ficheros, esta API, este CMS— y Astro se adapta, ofreciendo siempre el mismo acceso tipado por encima. La colección deja de ser “una carpeta que Astro lee” y pasa a ser “un contrato sobre un origen que tú eliges”. Esta idea —separar la interfaz estable de consumo de la fuente cambiante de los datos— es uno de los patrones más fértiles de la ingeniería: es lo que hace un repositorio frente a una tabla, un driver frente a un dispositivo, un adaptador frente a un servicio. El valor no es poder cambiar de fuente cada día, que rara vez harás, sino que el resto de tu código no sepa ni le importe de dónde vienen los datos. Escribes getCollection('blog') igual tanto si el blog son cien Markdown locales como si es una consulta a un CMS a mil kilómetros. Cuando entiendes que el loader es esa costura —el punto donde el mundo desordenado de las fuentes se convierte en el mundo ordenado de las entradas tipadas— dejas de ver content.config.ts como un fichero de ajustes y empiezas a verlo como la frontera donde defines, en tus propios términos, qué es el contenido de tu sitio.

⚔️ Declara tu primera colección
  1. Crea src/content.config.ts y define una colección blog con un glob loader que apunte a ./src/content/blog.
  2. Añade dos o tres Markdown en esa carpeta y comprueba, con getCollection('blog'), que aparecen como entradas.
  3. Cambia el pattern a **/*.{md,mdx} y verifica que ahora también entran los .mdx.
  4. Estropea el base a propósito y observa que la colección queda vacía sin error; razona por qué el loader es el primer sospechoso cuando faltan entradas.