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.
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.
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.
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.