Hasta ahora, hemos puesto nuestros archivos Markdown (.md) directamente en la carpeta src/pages. Esto funciona bien para sitios pequeños, donde el archivo creaba la ruta automáticamente.
Pero este enfoque tiene problemas graves cuando el proyecto crece:
- Falta de Validación: Si en un artículo pones
date: "2023-01-01"y en otro te olvidas, o ponesfecha: "Enero", tu código de ordenación explotará. - Sin Autocompletado: Tu editor no sabe qué datos tiene tu Markdown. Tienes que memorizar si llamaste al campo
image,img,coverothumbnail. - Mezcla de conceptos:
src/pagesdebería ser para el enrutamiento, no para almacenar tu base de datos de contenido.
Astro soluciona esto con las Content Collections.
Las Content Collections cambiaron Astro para siempre en la versión 2.0. Antes, gestionar un blog grande era un campo de minas: si escribías mal una fecha o te olvidabas de poner la imagen destacada en el frontmatter, la web podía romperse en producción o mostrar datos vacíos.
La Carpeta src/content
Astro reserva una carpeta especial en la raíz de src llamada content.
Todo lo que pongas aquí no generará rutas automáticamente. Es decir, si pones src/content/blog/mi-post.md, no podrás acceder a /blog/mi-post sin más.
Esto es bueno, porque nos permite separar los datos (el contenido) de la presentación (cómo se muestra).
Una estructura típica se ve así:
src/ ├── content/ │ ├── config.ts # Archivo clave │ ├── blog/ # Colección “blog” │ │ ├── post-1.md │ │ └── post-2.mdx │ └── autores/ # Colección “autores” │ ├── luis.json │ └── ana.json
Configuración y esquemas con Zod
La definición de las colecciones se realiza en el archivo src/content/config.ts. Aquí es donde definimos las reglas.
Astro utiliza Zod, una librería de validación de esquemas de TypeScript muy popular, para asegurar que nuestro frontmatter sea perfecto.
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
// 1. Definimos el esquema para la colección 'blog'
const blogCollection = defineCollection({
type: 'content', // 'content' para markdown/mdx, 'data' para json/yaml
schema: z.object({
title: z.string(),
description: z.string().max(160, "¡La descripción es muy larga para SEO!"),
pubDate: z.date(),
draft: z.boolean().default(false),
// Zod puede validar que la imagen exista realmente en tu carpeta
cover: z.string().optional(),
tags: z.array(z.string()),
}),
});
// 2. Definimos el esquema para la colección 'autores'
const autoresCollection = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
role: z.string(),
twitter: z.string().url(),
})
});
// 3. Exportamos las colecciones para registrarlas
export const collections = {
'blog': blogCollection,
'autores': autoresCollection,
};
Si ahora intentas crear un archivo Markdown en src/content/blog/ y te olvidas del title, o pones un texto en pubDate en lugar de una fecha válida… ¡Astro no compilará!
En su lugar te mostrará un error en rojo gigante en la terminal indicándote exactamente qué archivo está mal y por qué. (adiós a los errores en producción).
Consultando las colecciones (getCollection)
Como los archivos en src/content no generan rutas, tenemos que “pedirlos” nosotros mediante código.
Para ello usamos las funciones getCollection y getEntry de astro:content.
Imagina que queremos mostrar una lista de artículos en nuestro index.astro:
---
// src/pages/index.astro
import { getCollection } from 'astro:content';
import Card from '../components/Card.astro';
// Obtenemos TODOS los posts del blog
// Podemos filtrar directamente dentro de la función
const posts = await getCollection('blog', ({ data }) => {
return data.draft === false; // Solo mostramos los que no son borradores
});
---
<h1>Mi Blog</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>
{post.data.title}
</a>
<p>{post.data.description}</p>
</li>
))}
</ul>
Fíjate que accedemos a los valores a través de la propiedad .data. post.slug es la URL amigable del archivo. post.body contiene el contenido en crudo (raw). post.data contiene el frontmatter validado.
Generando rutas dinámicas
Ahora que tenemos los datos, necesitamos crear las páginas individuales para cada artículo (ej: /blog/mi-post).
Para esto usamos las Rutas Dinámicas de Astro en combinación con las colecciones. Creamos un archivo con corchetes en src/pages/blog/[...slug].astro.
---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
// 1. getStaticPaths es OBLIGATORIO en modo estático (SSG)
// Le dice a Astro qué páginas debe generar
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post }, // Pasamos el objeto post entero como prop
}));
}
// 2. Recogemos el post de las props
const { post } = Astro.props;
// 3. Renderizamos el contenido
// Esta función especial procesa el Markdown/MDX a HTML
const { Content } = await post.render();
---
<Layout title={post.data.title}>
<h1>{post.data.title}</h1>
<p>Publicado el: {post.data.pubDate.toLocaleDateString()}</p>
<article class="prose">
<Content />
</article>
</Layout>
- Astro ejecuta
getStaticPathsdurante el build. - Llama a
getCollection('blog')y recupera 100 artículos. - Genera 100 rutas:
/blog/articulo-1,/blog/articulo-2, etc. - Para cada ruta, ejecuta el componente, renderiza el
<Content />y guarda el HTML final.
Relaciones entre colecciones
Una de las características más potentes y recientes es la capacidad de relacionar colecciones, similar a una base de datos relacional (SQL).
Imagina que cada post del blog tiene un autor, y ese autor está en la colección autores.
En src/content/config.ts:
const blogCollection = defineCollection({
schema: ({ image, reference }) => z.object({
title: z.string(),
// Referenciamos la colección 'autores' por su ID (nombre del archivo)
author: reference('autores'),
}),
});
Y luego, al renderizar el post puedes hacer:
---
import { getEntry } from 'astro:content';
const { post } = Astro.props;
// Recuperamos los datos del autor usando la referencia
const authorData = await getEntry(post.data.author);
---
<p>Escrito por: {authorData.data.name}</p>
<a href={authorData.data.twitter}>Seguir en Twitter</a>
