wandres.dev
RUTAS DINÁMICAS · params y getStaticPaths

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.

⏱ 16 min

Todo lo anterior asumía un sitio estático: rutas horneadas en el build, un universo cerrado que getStaticPaths enumera de antemano. Pero hay contenidos que no caben en esa foto fija —un catálogo de millones de productos, un perfil por usuario, páginas que nacen después del despliegue—. El renderizado bajo demanda cambia las reglas: hay un servidor vivo que atiende cada petición, así que ya no hace falta enumerar nada. La página lee Astro.params en el instante de la visita, resuelve el valor contra sus datos en tiempo real, y decide sobre la marcha si hay algo que mostrar o si toca un 404.

🎯 Al terminar esta lección sabrás
  • Convertir una ruta dinámica en bajo demanda con export const prerender = false.
  • Leer Astro.params en tiempo de petición, sin getStaticPaths.
  • Resolver el parámetro contra una fuente de datos consultada en vivo.
  • Devolver un 404 explícito cuando el recurso pedido 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].astro
export const prerender = false;   // esta ruta se renderiza por peticion

const { 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.

ℹ️
prerender false por ruta, o servidor por defecto

Hay dos formas de llegar a lo mismo. Si tu proyecto es estático por defecto, export const prerender = false saca una ruta concreta de esa norma y la vuelve bajo demanda. Si configuras el proyecto entero en modo servidor, todas las rutas son bajo demanda por defecto y usas export const prerender = true para congelar las que sí convenga hornear. El eje —build o petición— se decide por ruta, y puedes mezclarlo: un blog estático con una sola sección dinámica, o un sitio dinámico con su portada precomputada.

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.

📝
Un 404 no es lo mismo que un 500

La distinción de códigos no es cosmética. Un 404 comunica este recurso no existe, una respuesta legítima y esperable ante una URL que no corresponde a nada; los buscadores la entienden y no penalizan. Un 500 comunica algo se ha roto por dentro, un fallo del servidor que sí es un problema real. Cuando olvidas la guarda y dejas que la plantilla lea un dato nulo, conviertes una situación normal —una dirección inexistente— en un error del sistema. Emitir el código correcto es, en el fondo, decir la verdad sobre lo que ha pasado.

flowchart TD
REQ[peticion a productos 42] --> EJ[el servidor ejecuta id punto astro]
EJ --> LEE[lee Astro params id]
LEE --> Q[consulta la fuente en vivo]
Q --> EX{existe el recurso}
EX -->|si| OK[renderiza la pagina]
EX -->|no| NF[devuelve 404 explicito]
style REQ fill:#89b4fa,color:#11111b
style OK fill:#a6e3a1,color:#11111b
style NF fill:#f38ba8,color:#11111b
⚠️
Sin la guarda, un valor inventado te da un 500

El fallo más común al pasar de estático a bajo demanda es olvidar el chequeo del recurso ausente. En estático nunca hacía falta, porque las rutas inexistentes no llegaban a ejecutar tu código. Bajo demanda sí lo ejecutan, con el valor que sea. Si no compruebas que la consulta devolvió algo antes de usarlo, cualquier URL malintencionada o simplemente equivocada dispara una excepción y un 500. La guarda no es opcional: es la que convierte una ruta frágil en una que responde con dignidad a lo inesperado.

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.

🏭

Estático: horneado

getStaticPaths enumera en el build. Universo cerrado, respuestas instantaneas, garantias por adelantado.

🛎️

SSR: al vuelo

Sin enumerar; el servidor resuelve cada peticion. Universo abierto, contenido fresco, sin limite de rutas.

🛡️

La guarda del 404

Bajo demanda debes comprobar que el recurso existe y emitir un 404 honesto cuando no.

⚖️

Elegir por naturaleza

Conocido y acotado pide estatico; ilimitado, fresco o personalizado pide servidor.

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.

Estático y SSR son el mismo cálculo evaluado en momentos distintos

Al final de este nivel conviene ver que las dos modalidades de ruta dinámica no son dos mecanismos rivales, sino la misma función evaluada en dos momentos. Un fichero con corchetes siempre fue una función de parámetro a página. En estático, aplicas esa función a todos sus argumentos por adelantado y guardas la tabla de resultados: es evaluación temprana, ansiosa, con toda la respuesta lista antes de la primera visita. Bajo demanda, dejas la función sin evaluar y la aplicas al argumento exacto cuando llega: es evaluación tardía, perezosa, calculada en el punto de uso. Es la misma dualidad que recorre la computación entera —precalcular contra calcular al vuelo, cachear contra recomputar, compilar contra interpretar— y cada lado hereda su carácter de esa naturaleza. La evaluación temprana compra velocidad y certeza a cambio de rigidez: si está en la tabla es instantáneo e infalible, pero la tabla tuvo que caber y hubo que conocerla entera. La evaluación tardía compra flexibilidad y frescura a cambio de trabajo por visita y de responsabilidad: puede atender un universo infinito y siempre actual, pero cada respuesta cuesta cómputo en vivo y, sobre todo, ahora eres tú quien debe manejar el caso en que la función no tiene respuesta —el recurso que no existe, el 404 que en estático era automático y aquí es una decisión que tomas—. Entender rutas dinámicas a fondo no es memorizar dos APIs, sino reconocer que estás eligiendo cuándo evaluar, y que ese cuándo arrastra todo lo demás: el rendimiento, las garantías, quién carga con los errores. El día que ves getStaticPaths y prerender = false como dos ajustes del mismo dial —cuándo se calcula esta página— dejas de elegirlos por receta y empiezas a elegirlos por naturaleza del contenido.

⚔️ Resolver rutas en tiempo de petición
  1. Crea src/pages/productos/[id].astro, márcala con export const prerender = false y muestra Astro.params.id leído al vuelo.
  2. Consulta una fuente de datos con ese id y pinta el recurso; verifica que la página se genera en cada petición, no en el build.
  3. Añade la guarda: si la consulta no devuelve nada, corta con new Response(null, { status: 404 }) y comprueba el estado.
  4. Sustituye ese Response por Astro.rewrite('/404') y razona cuándo prefieres una respuesta escueta y cuándo una página de error con maqueta.