Content Layer Avanzado, Loaders y Headless CMS
En Astro 7 utilizamos Content Layer, el mismo sistema de colecciones con loaders que ya hemos usado en el blog: permite consultar, validar y tipar datos procedentes de cualquier fuente (archivos locales, APIs remotas, bases de datos o Headless CMS) exactamente con la misma API unificada.
1. Colecciones de compilación y sus loaders #
Las colecciones pueden cargar archivos locales o datos remotos. En este apartado ampliamos el loader del blog; no cambiamos de API.
Con Content Layer:
- El archivo de configuración es 📄
src/content.config.ts(en la raíz desrc/). - Cada colección define un
loader, encargado de suministrar los datos desde cualquier origen. - Todo el contenido se valida con esquemas Zod y se almacena en una caché optimizada en tiempo de compilación.
2. Los Loaders integrados (glob y file) #
Para contenido en archivos locales, Astro incluye loaders optimizados que no están restringidos a una sola carpeta:
📄 src/content.config.ts
1import { defineCollection } from 'astro:content'; 2import { z } from 'astro/zod'; 3import { glob, file } from 'astro/loaders'; 4 5// Colección de artículos usando archivos locales con loader glob 6const blog = defineCollection({ 7 loader: glob({ pattern: '**/*.{md,mdx}', base: './src/data/blog' }), 8 schema: ({ image }) => z.object({ 9 title: z.string(), 10 description: z.string(), 11 pubDate: z.coerce.date(), 12 cover: image().optional(), 13 tags: z.array(z.string()).default([]), 14 }), 15}); 16 17// Colección desde un único archivo JSON o YAML 18const paises = defineCollection({ 19 loader: file('./src/data/paises.json'), 20 schema: z.object({ 21 id: z.string(), 22 nombre: z.string(), 23 capital: z.string(), 24 }), 25}); 26 27export const collections = { blog, paises };
3. Cargar datos desde Headless CMS y APIs remotas #
Puedes crear un loader asíncrono personalizado para conectar cualquier API REST, GraphQL o CMS (Strapi, Contentful, WordPress, Notion, GitHub):
1// src/content.config.ts 2import { defineCollection } from 'astro:content'; 3import { z } from 'astro/zod'; 4 5const productos = defineCollection({ 6 loader: async () => { 7 const respuesta = await fetch('https://api.tienda.com/productos'); 8 if (!respuesta.ok) throw new Error(`Error del CMS: ${respuesta.status}`); 9 const datos = await respuesta.json(); 10 11 // Cada elemento debe devolver obligatoriamente un campo `id` único 12 return datos.map((prod: any) => ({ 13 id: prod.slug, 14 nombre: prod.title, 15 precio: prod.price, 16 categoria: prod.category, 17 })); 18 }, 19 schema: z.object({ 20 id: z.string(), 21 nombre: z.string(), 22 precio: z.number().positive(), 23 categoria: z.enum(['electronica', 'ropa', 'hogar']), 24 }), 25}); 26 27export const collections = { productos };
En tus páginas, lo consultas igual que si fuera un archivo Markdown local:
1--- 2// src/pages/productos/index.astro 3import { getCollection } from 'astro:content'; 4const items = await getCollection('productos'); 5--- 6<ul> 7 {items.map(item => <li>{item.data.nombre} — {item.data.precio} €</li>)} 8</ul>
4. Referencias cruzadas entre colecciones (reference) #
Puedes relacionar entidades fácilmente (por ejemplo, asignar un autor a un artículo):
1// src/content.config.ts 2import { defineCollection, reference } from 'astro:content'; 3import { z } from 'astro/zod'; 4import { glob } from 'astro/loaders'; 5 6const autores = defineCollection({ 7 loader: glob({ pattern: '**/*.json', base: './src/data/autores' }), 8 schema: z.object({ 9 nombre: z.string(), 10 avatar: z.url(), 11 twitter: z.string().optional(), 12 }), 13}); 14 15const articulos = defineCollection({ 16 loader: glob({ pattern: '**/*.md', base: './src/data/articulos' }), 17 schema: z.object({ 18 title: z.string(), 19 autor: reference('autores'), // Relación foránea tipada 20 }), 21}); 22 23export const collections = { autores, articulos };
Para obtener los datos del autor en la plantilla:
1--- 2import { getEntry } from 'astro:content'; 3 4const post = Astro.props.post; 5const autor = await getEntry(post.data.autor); 6--- 7<p>Escrito por: {autor.data.nombre}</p>
5. Tabla de contenidos automática (headings) #
Al renderizar un post con render(post), Astro extrae automáticamente todos los títulos <h1>, <h2>, <h3>:
1--- 2import { render } from 'astro:content'; 3const { post } = Astro.props; 4const { Content, headings } = await render(post); 5--- 6 7<aside class="tabla-contenidos"> 8 <h3>En este artículo</h3> 9 <ul> 10 {headings.map(h => ( 11 <li class={`depth-${h.depth}`}> 12 <a href={`#${h.slug}`}>{h.text}</a> 13 </li> 14 ))} 15 </ul> 16</aside> 17 18<article> 19 <Content /> 20</article>
6. Personalizar Markdown con plugins de Remark y Rehype #
Astro 7 utiliza Sätteri como procesador predeterminado para Markdown/MDX. Si necesitas plugins de Remark/Rehype, elige explícitamente el procesador Unified e instala sus dependencias:
1npm install @astrojs/markdown-remark remark-gfm rehype-slug rehype-autolink-headings
Este ejemplo añade enlaces a los encabezados. Las tablas GFM ya están soportadas de forma predeterminada; no instales plugins si no los necesitas. Conserva las integraciones, plugins de Vite y el adaptador que ya tengas; este apartado solo cambia markdown.
📄 astro.config.mjs
1import { defineConfig } from 'astro/config'; 2import { unified } from '@astrojs/markdown-remark'; 3import remarkGfm from 'remark-gfm'; 4import rehypeSlug from 'rehype-slug'; 5import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 6 7export default defineConfig({ 8 markdown: { 9 processor: unified({ 10 remarkPlugins: [remarkGfm], 11 rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]], 12 }), 13 shikiConfig: { 14 theme: 'dracula', 15 wrap: true, 16 }, 17 }, 18});
7. Cuando el contenido debe cambiar entre visitas #
Un loader remoto de src/content.config.ts consulta el CMS durante la compilación: getCollection() no vuelve a consultar esa API en cada visita, ni siquiera si la plantilla se renderiza con SSR.
Las Live Content Collections son una alternativa para datos que deben consultarse bajo demanda:
| Decisión | Colección de compilación | Colección en vivo |
|---|---|---|
| Configuración | src/content.config.ts | src/live.config.ts |
| Definición | defineCollection() | defineLiveCollection() |
| Consulta | getCollection() / getEntry() | getLiveCollection() / getLiveEntry() |
| Loader | glob, file o personalizado | Loader en vivo con loadCollection / loadEntry |
| Ejecución | Durante el build | Durante la petición; requiere adaptador y ruta bajo demanda |
Para un blog o portfolio utiliza las colecciones de compilación. Para existencias de una tienda puede ser más adecuado un loader en vivo. Este último debe manejar errores del origen y la página debe contemplar el resultado de la consulta antes de renderizarlo. Guía de colecciones en vivo.
Resumen del tema
Conceptos clave #
- Content Layer (
src/content.config.ts): arquitectura universal en Astro para desacoplar el origen de los datos de su consumo en plantillas. - Loaders integrados (
glob,file): mecanismos de ingesta de contenido en múltiples formatos y directorios. - Carga de CMS remotos: loaders con funciones asíncronas que normalizan datos de APIs REST/GraphQL devolviendo objetos con
id. - Validaciones avanzadas con Zod: coerción de fechas (
z.coerce.date()), enumeraciones y tipos de imagen optimizados (image()). - Extracción de metadatos: acceso a la jerarquía de títulos (
headings) para tablas de contenido inmediatas.
Qué debes recordar #
Configura colecciones en src/content.config.ts usando loaders para ingerir contenido tipado de Markdown, JSON o Headless CMS con una única API getCollection().