Rutas dinámicas en SSR
Cuando el universo de rutas no cabe en un build: en renderizado bajo demanda no hace falta getStaticPaths, porque el servidor resuelve el parámetro en el momento de cada petición. Cómo leer Astro.params al vuelo, consultar la fuente de datos en vivo y devolver un 404 honesto cuando el recurso no existe.
Sin getStaticPaths: el parámetro se resuelve al vuelo
En un sitio estático, un fichero con corchetes exigía getStaticPaths porque no había servidor: alguien tenía que decidir en el build qué páginas existían. En renderizado bajo demanda esa exigencia desaparece. Hay un servidor atendiendo peticiones, y cuando llega una a /productos/42, ejecuta el fichero [id].astro en ese momento, con id valiendo "42". No se enumera por adelantado; se resuelve al vuelo.
Para que una ruta se renderice así —en un proyecto que por defecto es estático— la marcas explícitamente. Con un adaptador de servidor instalado, basta una línea en el frontmatter:
---// src/pages/productos/[id].astroexport const prerender = false; // esta ruta se renderiza por peticionconst { id } = Astro.params;const producto = await buscarProducto(id);---<h1>{producto.nombre}</h1>Sin getStaticPaths, Astro.params sigue funcionando igual: te entrega el valor del segmento. La diferencia es cuándo lo hace. Antes, ese valor era uno de los que tú habías enumerado en el build; ahora es lo que sea que el visitante haya puesto en la URL, capturado en vivo. El fichero deja de ser una plantilla precocinada y vuelve a ser lo que su forma sugería desde el principio: una función que se ejecuta con el argumento que le llega.
Resolver contra los datos y decidir el 404
La libertad de aceptar cualquier valor trae una obligación nueva: comprobar que ese valor corresponde a algo real. En estático el problema no existía —solo se generaban las rutas válidas, y lo demás era 404 automático—. Bajo demanda, en cambio, el servidor ejecuta tu fichero para cualquier id, incluido uno inventado. Eres tú quien debe distinguir el recurso que existe del que no.
El patrón es una consulta seguida de una guarda. Buscas el dato con el parámetro; si no aparece, cortas y devuelves un 404 antes de intentar pintar nada.
---export const prerender = false;const { id } = Astro.params;const producto = await buscarProducto(id);if (!producto) { return new Response(null, { status: 404 });}---<h1>{producto.nombre}</h1><p>{producto.descripcion}</p>Ese if es el corazón de una ruta dinámica bajo demanda. Sin él, un id inexistente haría que producto fuese nulo y la plantilla reventaría al leer producto.nombre de algo que no está: un error 500 turbio en lugar de un 404 honesto. La diferencia entre ambos importa —un 404 dice esto no existe, un 500 dice me he roto—, y solo tú, que conoces tus datos, puedes emitir el correcto.
Astro ofrece más de una manera de responder ese 404. Puedes devolver un Response con estado 404, como arriba, o reescribir hacia tu página de error para reutilizar su diseño:
---export const prerender = false;const usuario = await buscarUsuario(Astro.params.handle);if (!usuario) { return Astro.rewrite('/404');}---Astro.rewrite renderiza el contenido de otra ruta —tu src/pages/404.astro— manteniendo la URL que pidió el visitante, de modo que el error sale con su maqueta completa. Elegir entre un Response escueto y un rewrite con plantilla depende de si quieres una respuesta mínima o una página de error cuidada; ambas cierran correctamente el caso del recurso ausente.
Universo abierto frente a universo cerrado
La distinción de fondo entre los dos modos es el tamaño y el momento del universo de rutas. En estático es cerrado y anticipado: existe exactamente lo que getStaticPaths enumeró en el build, ni una URL más. Bajo demanda es abierto y diferido: existe cualquier URL que case con el patrón, y qué haya detrás lo decide una consulta en el momento de pedirla.
Ninguno es mejor en abstracto; sirven a contenidos distintos. Si el conjunto de páginas es conocido y acotado en el momento del build —los artículos de un blog, las páginas de una documentación—, el estático gana: precomputas una vez, sirves ficheros planos, y disfrutas de la certeza de saber qué existe. Si el conjunto es ilimitado, cambia a cada segundo o depende de quién visita —un catálogo enorme, resultados de búsqueda, un panel personalizado—, enumerarlo en el build es imposible o absurdo, y el renderizado bajo demanda es la respuesta natural.
Y lo mejor es que la elección no es global ni irreversible. prerender se decide ruta por ruta, así que un mismo proyecto puede hornear su portada y su blog, y resolver bajo demanda su buscador y sus perfiles. El fichero [id].astro es casi idéntico en ambos mundos —lee Astro.params, usa el valor—; lo que cambia es si ese valor vino de una lista precalculada o de la petición del instante.
Esa casi identidad del fichero es una pista de diseño valiosa: escribe la lógica de una ruta pensando primero en el parámetro y el dato, y deja la decisión de cuándo resolverlos —build o petición— para el final, como un ajuste y no como una reescritura. Cuanto más neutral sea tu código respecto al momento de ejecución, más barato te resultará mover una ruta de un modo al otro el día que el contenido lo pida.