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.

Astro
17

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.

Código del tema: export async function getStaticPaths() {}

📘 Teoría

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:

---
titulo: Cartel para un festival de jazz
fecha: 2026-03-14
portada: ../../assets/cartel-jazz.png
textoAlternativo: Cartel naranja con un saxofón dibujado con líneas
etiquetas: [cartel, tipografía]
destacado: true
---

Cartel y adaptaciones para redes del festival Jazz en la Alameda.

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

  1. Crea src/content.config.ts con la colección proyectos y su esquema.
  2. Escribe tres proyectos en Markdown (inventados o tuyos), con al menos dos etiquetas cada uno.
  3. Lista los proyectos en la portada, del más nuevo al más antiguo.
  4. Crea src/pages/proyectos/[id].astro y comprueba que se abre la página de cada uno.
  5. Borra el titulo de un proyecto y lee el error que da. Vuelve a ponerlo.
  6. 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.

🧰 Recursos

Test

Comprueba tus conocimientos con un test sobre Astro.

Test de Astro

¿Qué es esto?

Soy Cristian Eslava y a veces hago webs para procrastinar yo y vosotros. culTest

La hice en febrero de 2026 para facilitar el aprendizaje de mis alumnos. La idea es aprender desarrollo web practicando y que el proyecto siga creciendo con nuevos temas, tests y retos.

Está inspirada en MDN, W3Schools, CodePen, Manz y muchos otros sitios de documentación sobre desarrollo web. Quería combinar teoría útil, ejemplos ejecutables, retos y el sistema de tests que ya tenía en culTest. culTest

Si te gustó, si no te gustó o si quieres escribirme, puedes hacerlo en cristianeslava@gmail.com