wandres.dev
CONTENT COLLECTIONS · glob loader y schema

Referencias, imágenes y patrones de blog

Enlazar colecciones entre sí con reference y resolverlas con getEntry y getEntries, validar y optimizar imágenes desde el esquema con el ayudante image, y modelar un blog real con autores y etiquetas como colecciones relacionadas con integridad comprobada en build.

⏱ 15 min

Una colección aislada ya es útil, pero el contenido real está entretejido: un post tiene un autor, unas etiquetas, una imagen de portada. Las content collections modelan esas relaciones sin salir del sistema de tipos. reference enlaza una colección con otra y comprueba en el build que el enlace apunta a algo real; image trae imágenes validadas y optimizables desde el propio esquema. Con esas dos piezas, un puñado de ficheros se convierte en un pequeño grafo relacional con integridad garantizada.

🎯 Al terminar esta lección sabrás
  • Enlazar colecciones con reference y resolver los enlaces con getEntry y getEntries.
  • Validar y optimizar imágenes desde el esquema con el ayudante image.
  • Modelar un blog con autores y etiquetas como colecciones relacionadas.
  • Reconocer las collections como un grafo relacional en ficheros, comprobado en build.

reference: enlazar colecciones

Un campo puede apuntar a una entrada de otra colección con reference. En el esquema declaras a qué colección referencia, y en el frontmatter escribes solo el id de la entrada destino.

Ese frontmatter queda limpio y legible: un post declara author: ada-lovelace y unas etiquetas por su nombre corto, identificadores que remiten a entradas ricas en otra parte. La complejidad del autor —su biografía, su avatar, sus enlaces— no ensucia el post; se guarda donde le corresponde y se referencia por su identificador. Astro guarda ese enlace y, durante el build, verifica que la entrada referenciada existe de verdad.

La referencia guarda un identificador, no una copia. Esa distinción es la clave de la normalización: el nombre y la biografía del autor viven una sola vez, en su entrada, y cada post apunta a ellos en lugar de duplicarlos. Cambiar un dato del autor es editar un fichero, y todos sus posts reflejan el cambio sin tocarse. Copiar el dato en cada frontmatter, en cambio, condena a que tarde o temprano las copias diverjan y ya nadie sepa cuál es la buena.

import { defineCollection, reference, 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(),
    author: reference('authors'),
    tags: z.array(reference('tags')),
  }),
});

Una referencia no trae los datos del destino, solo un puntero validado hacia él —un objeto con la colección y el id—. Para obtener la entrada completa, la resuelves con getEntry, que acepta directamente esa referencia. Cuando el campo es un array de referencias, getEntries las resuelve todas de una vez.

Resolver es un paso explícito y deliberado. Una entrada de blog no arrastra a su autor entero cada vez que la consultas —eso multiplicaría el trabajo—; trae un puntero ligero, y solo cuando de verdad necesitas los datos del autor pagas por resolverlo. Así, una portada que lista títulos no carga cincuenta biografías, y la página de detalle, que sí muestra el autor, lo resuelve una vez. El grafo se recorre bajo demanda, no de golpe.

⚠️
Referenciar no es cargar

Un error común es esperar que post.data.author ya traiga el nombre del autor. No lo trae: contiene el puntero, no los datos. Si lo pintas directamente verás el identificador, no la persona. El dato completo solo aparece tras resolverlo con getEntry(post.data.author). Referenciar declara la relación; resolver la recorre.

import { getEntry, getEntries } from 'astro:content';

const post = await getEntry('blog', 'mi-post');
const autor = await getEntry(post.data.author);
const etiquetas = await getEntries(post.data.tags);

Lo decisivo es que el enlace se comprueba en el build: si un post apunta a un autor que no existe, el sitio no compila. La integridad referencial —que ningún enlace quede colgando— deja de depender de tu cuidado y pasa a ser una garantía de la máquina.

image: imágenes en el esquema

Para que un campo de imagen no sea una simple cadena de texto sino una imagen real, validada y lista para optimizar, el esquema se escribe en su forma de función, que recibe un ayudante image. Ese ayudante valida que la ruta apunte a un fichero existente y devuelve sus metadatos, listos para el componente <Image> de Astro.

La diferencia con una cadena de texto es enorme. Una URL suelta en el frontmatter no garantiza nada: puede apuntar a un fichero que borraste, y el fallo será un hueco en la página. Un image() valida en el build que el activo existe, conoce sus dimensiones —lo que permite reservar su espacio y evitar saltos de maquetación— y habilita la optimización: formatos modernos, tamaños responsivos, carga diferida. El campo pasa de ser una promesa frágil a un activo gestionado.

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      cover: image(),
      coverAlt: z.string(),
    }),
});

En la plantilla, ese cover ya no es una URL frágil sino un objeto con ancho, alto y formato que <Image> puede optimizar. Si la imagen referenciada no existe, el build falla, igual que con un campo obligatorio ausente. Emparejar image() con un coverAlt obligatorio es un buen hábito: convierte el texto alternativo en parte del contrato, no en un olvido frecuente.

Este es un ejemplo fino de cómo un esquema moldea buenas prácticas. Al declarar coverAlt obligatorio junto a cover, la accesibilidad deja de depender de la memoria de quien escribe: no hay portada sin texto alternativo, porque el build no lo permite. El esquema no solo valida datos; codifica las reglas que quieres que tu contenido cumpla siempre.

---
import { Image } from 'astro:assets';
import { getEntry } from 'astro:content';

const post = await getEntry('blog', 'mi-post');
---
<Image src={post.data.cover} alt={post.data.coverAlt} />

Un blog con autores y etiquetas

Reuniendo las piezas, un blog realista se modela como tres colecciones que se enlazan. Los posts viven en Markdown; los autores y las etiquetas, en datos estructurados que un glob sobre ficheros JSON convierte en entradas.

Definir esas colecciones es declarar tres loaders con sus esquemas en el mismo content.config.ts.

const authors = defineCollection({
  loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
  schema: z.object({
    name: z.string(),
    bio: z.string(),
  }),
});

Con authors y tags declaradas, el reference('authors') del esquema de blog ya tiene un destino válido contra el que comprobarse. El orden en el fichero no importa; lo que importa es que toda colección referenciada exista, porque de eso depende que el build pueda validar los enlaces.

📝

blog

Los posts en Markdown. Cada uno referencia un autor y varias etiquetas, y valida su portada con image.

👤

authors

Una entrada por autor, con su nombre y su biografía. Se referencia desde muchos posts sin duplicarse.

🏷️

tags

Una entrada por etiqueta, con su nombre y su descripción. Agrupa posts y da pie a páginas por etiqueta.

Un dato del autor —su nombre, su avatar— vive en un solo sitio y lo comparten todos sus posts por referencia. Corregir su biografía es editar una entrada, no cien frontmatters. Y como las etiquetas son una colección, cada una puede tener su propia página que lista los posts que la referencian, construida con getCollection y un filtro sobre data.tags.

Esa página por etiqueta invierte la referencia: en lugar de ir del post a su etiqueta, va de la etiqueta a sus posts, filtrando la colección blog por quienes la mencionan. La misma relación se recorre en ambos sentidos según lo que quieras mostrar, sin duplicar ningún dato. Autores, etiquetas y posts forman un grafo que puedes consultar desde cualquiera de sus vértices.

flowchart LR
BLOG[entrada de blog] --> A[author como reference]
BLOG --> T[tags como array de reference]
A --> RA[getEntry resuelve el autor]
T --> RT[getEntries resuelve las etiquetas]
BLOG --> IMG[cover validado con image]
style BLOG fill:#89b4fa,color:#11111b
style RA fill:#a6e3a1,color:#11111b
style RT fill:#a6e3a1,color:#11111b
style IMG fill:#a6e3a1,color:#11111b
Las collections son una base de datos relacional en ficheros

Cuando reference entra en escena, lo que tienes entre manos deja de parecerse a una carpeta de textos y empieza a parecerse a lo que de verdad es: una base de datos relacional cuyas tablas son colecciones, cuyas filas son entradas y cuyas claves foráneas son referencias, todo escrito en ficheros y verificado en el build. Cada idea que la ingeniería de datos maduró durante décadas reaparece aquí, traducida al mundo del contenido. La normalización —guardar cada hecho una sola vez— es lo que consigues al poner el autor en su propia colección y referenciarlo, en lugar de copiar su nombre en cada post: hay una única fuente de verdad para cada dato, y actualizarla es cambiar un solo lugar. La integridad referencial —que ningún enlace apunte al vacío— es lo que Astro comprueba al negarse a compilar un post cuyo autor no existe: las relaciones rotas se detectan antes de desplegar, no cuando un visitante encuentra una ficha vacía. Y la separación entre el dato y su forma, que ya viste con render, se extiende ahora a las imágenes: image convierte una ruta en un activo validado y optimizable, cerrando la última grieta por la que se colaban los errores a mano. La lección que corona todo el nivel es esta: las content collections no son un cargador de Markdown con esteroides, sino un sistema para tratar el contenido de un sitio con el mismo rigor con que trataríamos los datos de una aplicación. Esquemas que definen la forma, validación que la garantiza, referencias que modelan las relaciones, integridad que la máquina vigila. Cuando piensas tu contenido así —como un grafo tipado de datos relacionados, no como una pila de ficheros— ganas justo lo que da una buena base de datos: la certeza de que lo que consultas es coherente, y la libertad de construir sobre esa certeza sin volver a comprobarla nunca a mano.

⚔️ Modela un blog relacionado
  1. Crea tres colecciones —blog, authors y tags— y enlaza cada post a un autor con reference('authors') y a varias etiquetas con z.array(reference('tags')).
  2. En la página de un post, resuelve el autor con getEntry y las etiquetas con getEntries, y muestra sus datos completos.
  3. Apunta un post a un autor inexistente y comprueba que el build se detiene; corrige el id y verifica que vuelve a compilar.
  4. Añade un campo cover con image() y un coverAlt obligatorio, píntalo con <Image>, y razona por qué validar la imagen en el esquema evita enlaces rotos en producción.