Astro a fondo 2, rutas dinámicas y colecciones de contenido
Un portafolio crece: cada proyecto nuevo no debería pedirte una página .astro nueva.
Cada fichero de src/pages/ es una página; con corchetes, una ruta dinámica que necesita getStaticPaths().
Una colección se declara en src/content.config.ts con un loader y un esquema de Zod.
getCollection() trae las entradas y render() convierte su Markdown en <Content />.
Un Markdown nuevo en la carpeta de la colección = una tarjeta y una página nuevas.
- Un portafolio crece: cada proyecto nuevo no debería pedirte una página .astro nueva. Escribes un Markdown con sus datos y Astro genera la tarjeta de la portada y la página del proyecto. Para eso están las rutas dinámicas y las colecciones de contenido.
- Fichero en src/pages/Dirección index.astro/ sobre-mi.astro/sobre-mi proyectos/index.astro/proyectos proyectos/[id].astro/proyectos/cartel-festival-jazz, /proyectos/marca-cafeteria… 404.astroLa página de «no encontrado»
- Los corchetes marcan una ruta dinámica: un solo fichero que genera muchas páginas. En una web estática, Astro tiene que saber al construir qué páginas existen, y se lo dices con getStaticPaths():
- --- // src/pages/proyectos/[id].astro (versión mínima) export function getStaticPaths() { return [ { params: { id: 'cartel-festival-jazz' } }, { params: { id: 'marca-cafeteria' } }, ]; } const { id } = Astro.params; --- <h1>Proyecto: {id}</h1>
- Cada objeto de la lista es una página. params da el trozo de la dirección; con props le pasas además los datos que necesita, y la página los lee en Astro.props. Escribir la lista a mano no escala: la colección la genera por ti.
Introducción
Un portafolio crece: cada proyecto nuevo no debería pedirte una página .astro nueva. Escribes un Markdown con sus datos y Astro genera la tarjeta de la portada y la página del proyecto. Para eso están las rutas dinámicas y las colecciones de contenido.
Rutas: un fichero, una dirección
Fichero en src/pages/Dirección index.astro/ sobre-mi.astro/sobre-mi proyectos/index.astro/proyectos proyectos/[id].astro/proyectos/cartel-festival-jazz, /proyectos/marca-cafeteria… 404.astroLa página de «no encontrado»
Los corchetes marcan una ruta dinámica: un solo fichero que genera muchas páginas. En una web estática, Astro tiene que saber al construir qué páginas existen, y se lo dices con getStaticPaths():
--- // src/pages/proyectos/[id].astro (versión mínima) export function getStaticPaths() { return [ { params: { id: 'cartel-festival-jazz' } }, { params: { id: 'marca-cafeteria' } }, ]; } const { id } = Astro.params; --- <h1>Proyecto: {id}</h1>
Cada objeto de la lista es una página. params da el trozo de la dirección; con props le pasas además los datos que necesita, y la página los lee en Astro.props. Escribir la lista a mano no escala: la colección la genera por ti.
La colección de contenido
Una colección es un grupo de contenidos del mismo tipo (proyectos, entradas de un blog, miembros de un equipo) con los campos que tú defines. Se declara en src/content.config.ts:
// src/content.config.ts import { defineCollection } from 'astro:content'; import { glob } from 'astro/loaders'; import { z } from 'astro/zod'; const proyectos = defineCollection({ loader: glob({ base: './src/content/proyectos', pattern: '**/*.md' }), schema: ({ image }) => z.object({ titulo: z.string(), fecha: z.coerce.date(), portada: image(), textoAlternativo: z.string(), etiquetas: z.array(z.string()).default([]), destacado: z.boolean().default(false), }), }); export const collections = { proyectos };
El loader dice de dónde salen los datos. glob() lee ficheros de una carpeta; file() lee un JSON o YAML con una lista (cada elemento con su id). El esquema (schema) dice qué campos lleva cada proyecto y de qué tipo. Lo escribes con Zod (z), una librería de validación que trae Astro. z.coerce.date() convierte 2026-03-14 en una fecha; .default([]) pone un valor si falta el campo; image() comprueba que la imagen existe y la prepara para optimizarla.
Cada proyecto es un Markdown en src/content/proyectos/. Los datos van en la cabecera (el frontmatter, entre ---) y el texto, debajo:
El encargo
Tres tamaños: A2 para la calle, 1080 × 1350 para Instagram y una cabecera para la web.
Si un proyecto no tiene `titulo` o la fecha está mal escrita, `npm run dev` y `npm run build` paran con un error que dice qué fichero y qué campo fallan. Es la ventaja del esquema: te enteras al escribir, no cuando alguien ve la web rota. El `id` de cada entrada sale del nombre del fichero: `cartel-festival-jazz.md` → `cartel-festival-jazz`. Elige nombres de fichero que valgan como dirección (minúsculas, guiones, sin tildes).
Listar la colección en la portada
--- // src/pages/index.astro import { getCollection } from 'astro:content'; import Base from '../layouts/Base.astro'; import TarjetaProyecto from '../components/TarjetaProyecto.astro'; const proyectos = (await getCollection('proyectos')).sort( (a, b) => b.data.fecha.valueOf() - a.data.fecha.valueOf() ); --- <Base titulo="Proyectos"> <h1>Proyectos</h1> <div class="rejilla"> {proyectos.map((proyecto) => <TarjetaProyecto proyecto={proyecto} />)} </div> </Base>
getCollection('proyectos') devuelve todas las entradas. Los campos están en proyecto.data. .sort(...) las ordena de la más nueva a la más antigua. Para quedarte con algunas, un filtro: getCollection('proyectos', ({ data }) => data.destacado).
En la tarjeta, el tipo de la prop sale de la propia colección:
--- import type { CollectionEntry } from 'astro:content'; interface Props { proyecto: CollectionEntry<'proyectos'>; } const { proyecto } = Astro.props; const { titulo, etiquetas } = proyecto.data; --- <article class="tarjeta"> <h2><a href={`/proyectos/${proyecto.id}`}>{titulo}</a></h2> <ul>{etiquetas.map((etiqueta) => <li>{etiqueta}</li>)}</ul> </article>
Una página por proyecto
getStaticPaths() recorre la colección; render() convierte el Markdown en un componente <Content />:
--- // src/pages/proyectos/[id].astro import { getCollection, render } from 'astro:content'; import Base from '../../layouts/Base.astro'; export async function getStaticPaths() { const proyectos = await getCollection('proyectos'); return proyectos.map((proyecto) => ({ params: { id: proyecto.id }, props: { proyecto }, })); } const { proyecto } = Astro.props; const { Content } = await render(proyecto); const fecha = proyecto.data.fecha.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' }); --- <Base titulo={proyecto.data.titulo}> <article> <h1>{proyecto.data.titulo}</h1> <p><time datetime={proyecto.data.fecha.toISOString()}>{fecha}</time></p> <Content /> </article> </Base>
Añade un Markdown a src/content/proyectos/ y aparecen su tarjeta y su página, sin tocar el código.
Practica
Crea src/content.config.ts con la colección proyectos y su esquema. Escribe tres proyectos en Markdown (inventados o tuyos), con al menos dos etiquetas cada uno. Lista los proyectos en la portada, del más nuevo al más antiguo. Crea src/pages/proyectos/[id].astro y comprueba que se abre la página de cada uno. Borra el titulo de un proyecto y lee el error que da. Vuelve a ponerlo. Crea src/pages/etiquetas/[etiqueta].astro que genere una página por etiqueta con sus proyectos. Pista: en getStaticPaths(), saca la lista de etiquetas sin repetir con new Set(...).
Errores frecuentes
Poner la configuración en src/content/config.ts, como en tutoriales antiguos. Desde Astro 6 es src/content.config.ts y cada colección necesita su loader. Importar z desde astro:content. Ahora se importa de astro/zod. Usar proyecto.slug o getEntryBySlug(): ya no existen. Es proyecto.id y getEntry(). Escribir proyecto.render(): ahora es render(proyecto), importado de astro:content. Olvidar getStaticPaths() en una ruta con corchetes: la construcción para con un error.