Content Layer Avanzado, Loaders y Headless CMS
Astro 5 introduce el Content Layer, una arquitectura revolucionaria que transforma el manejo de contenidos en la web: 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. De Content Collections al nuevo Content Layer #
En versiones anteriores, las colecciones estaban limitadas a archivos locales dentro de la carpeta src/content/.
Con Content Layer:
- El archivo de configuración pasa a ser 📄
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, z } from 'astro:content'; 2import { glob, file } from 'astro/loaders'; 3 4// Colección de artículos usando archivos locales con loader glob 5const blog = defineCollection({ 6 loader: glob({ pattern: '**/*.{md,mdx}', base: './src/data/blog' }), 7 schema: ({ image }) => z.object({ 8 title: z.string(), 9 description: z.string(), 10 pubDate: z.coerce.date(), 11 cover: image().optional(), 12 tags: z.array(z.string()).default([]), 13 }), 14}); 15 16// Colección desde un único archivo JSON o YAML 17const paises = defineCollection({ 18 loader: file('./src/data/paises.json'), 19 schema: z.object({ 20 id: z.string(), 21 nombre: z.string(), 22 capital: z.string(), 23 }), 24}); 25 26export 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, z } from 'astro:content'; 3 4const productos = defineCollection({ 5 loader: async () => { 6 const respuesta = await fetch('https://api.tienda.com/productos'); 7 const datos = await respuesta.json(); 8 9 // Cada elemento debe devolver obligatoriamente un campo `id` único 10 return datos.map((prod: any) => ({ 11 id: prod.slug, 12 nombre: prod.title, 13 precio: prod.price, 14 categoria: prod.category, 15 })); 16 }, 17 schema: z.object({ 18 id: z.string(), 19 nombre: z.string(), 20 precio: z.number().positive(), 21 categoria: z.enum(['electronica', 'ropa', 'hogar']), 22 }), 23}); 24 25export 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, z } from 'astro:content'; 3import { glob } from 'astro/loaders'; 4 5const autores = defineCollection({ 6 loader: glob({ pattern: '**/*.json', base: './src/data/autores' }), 7 schema: z.object({ 8 nombre: z.string(), 9 avatar: z.string().url(), 10 twitter: z.string().optional(), 11 }), 12}); 13 14const articulos = defineCollection({ 15 loader: glob({ pattern: '**/*.md', base: './src/data/articulos' }), 16 schema: z.object({ 17 title: z.string(), 18 autor: reference('autores'), // Relación foránea tipada 19 }), 20}); 21 22export 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() o post.render(), 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 #
En astro.config.mjs puedes enriquecer el procesamiento de Markdown con soporte para tablas GitHub (remark-gfm), enlaces en títulos (rehype-autolink-headings) y ecuaciones matemáticas (KaTeX):
1import { defineConfig } from 'astro/config'; 2import remarkGfm from 'remark-gfm'; 3import rehypeSlug from 'rehype-slug'; 4import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 5 6export default defineConfig({ 7 markdown: { 8 remarkPlugins: [remarkGfm], 9 rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]], 10 shikiConfig: { 11 theme: 'dracula', 12 wrap: true, 13 }, 14 }, 15});
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().