wandres.dev
RUTAS DINÁMICAS · params y getStaticPaths

Rutas catch-all con [...rest]

Cuando un solo segmento no basta: el operador de propagación en el nombre del fichero captura cero, uno o muchos tramos de la URL en un único parámetro. Cómo la cola llega como una cadena con barras, cómo trocearla para migas de pan, y qué reglas de precedencia deciden quién atiende una URL cuando varias podrían.

⏱ 15 min

Un [slug] captura exactamente un tramo de la URL. Pero hay rutas cuya profundidad no conoces de antemano: una documentación que anida secciones dentro de secciones, un explorador de archivos, una página comodín que atienda todo lo que sobre. Para eso existe el segmento catch-all: al preceder el nombre del corchete con tres puntos —[...rest]— dejas de capturar un tramo y pasas a capturar la cola entera de la URL, tenga los segmentos que tenga. Un fichero pasa de casar con una profundidad fija a casar con cualquier profundidad.

🎯 Al terminar esta lección sabrás
  • Declarar un segmento catch-all anteponiendo ... al nombre del corchete.
  • Leer la cola capturada como una única cadena con barras en Astro.params.
  • Trocear esa cadena en segmentos para construir migas de pan o navegación.
  • Entender la precedencia entre rutas estáticas, dinámicas y catch-all.

Los tres puntos: de un segmento a una cola entera

La diferencia entre [slug] y [...ruta] es la diferencia entre uno y muchos. src/pages/blog/[slug].astro casa con /blog/algo —un solo tramo— y no con /blog/algo/mas. En cambio src/pages/docs/[...ruta].astro casa con /docs, con /docs/guia, con /docs/guia/instalacion y con cualquier profundidad que le eches: el ... significa desde aquí, todo lo que venga.

src/pages/docs/[...ruta].astro  casa con:
  /docs                    ->  ruta = undefined
  /docs/guia               ->  ruta = "guia"
  /docs/guia/instalacion   ->  ruta = "guia/instalacion"
  /docs/api/v2/errores     ->  ruta = "api/v2/errores"

El nombre después de los puntos —ruta, rest, slug— vuelve a ser tu elección, y es la clave con la que leerás el valor. Lo que cambia radicalmente es lo que captura: no un tramo, sino la concatenación de todos los tramos restantes, unidos por las mismas barras que los separaban en la URL. Un [...rest] es, en esencia, una esponja que absorbe el resto del camino.

La diferencia de aridad frente a un corchete normal conviene tenerla nítida:

  • [slug] casa con exactamente un segmento: /blog/a sí, /blog/a/b no.
  • [...rest] casa con cero o más segmentos: /docs, /docs/a, /docs/a/b/c, todos.
  • Un [slug] entrega un valor atómico; un [...rest] entrega una cadena que tú decides cómo trocear.
ℹ️
Cero también cuenta como coincidencia

Un catch-all casa incluso cuando no queda nada que capturar. src/pages/docs/[...ruta].astro atiende también /docs a secas, y en ese caso el parámetro vale undefined, no una cadena vacía. Esta capacidad de casar con cero segmentos es lo que permite que un solo fichero cubra a la vez la raíz de una sección y todo su árbol interior. Conviene contemplar ese caso undefined explícitamente: suele ser la portada de la sección, y olvidarlo es la causa habitual de que /docs a secas falle mientras sus hijas funcionan.

La cola entera en un parámetro: leerla y trocearla

Dentro del fichero, la cola capturada vive en Astro.params bajo el nombre que elegiste, igual que cualquier parámetro. La diferencia es su contenido: una cadena que puede contener barras. Para /docs/guia/instalacion, Astro.params.ruta es la cadena "guia/instalacion" —un texto con una barra dentro—, no tres valores separados.

---
// src/pages/docs/[...ruta].astro
const { ruta } = Astro.params;
const segmentos = ruta?.split('/') ?? [];
---
<nav aria-label="migas">
  {segmentos.map((seg, i) => (
    <span>{seg}{i < segmentos.length - 1 ? ' / ' : ''}</span>
  ))}
</nav>

Ese split('/') es el gesto clave del patrón: reconvierte la cola aplanada en el array de tramos que la componía, y con ese array construyes lo que necesites. Las migas de pan son el caso canónico —cada segmento es un eslabón del rastro que llevó al visitante hasta aquí—, pero el mismo troceo sirve para resolver a qué documento apunta la ruta, para resaltar la sección activa en un menú lateral o para reconstruir enlaces acumulando prefijos. La cola es un dato compacto; trocearla es recuperar su estructura.

flowchart TD
U[url docs guia avanzada temas] --> C[archivo docs punto punto punto ruta]
C --> P[Astro params ruta igual guia barra avanzada barra temas]
P --> S[split por barra]
S --> M1[guia]
S --> M2[avanzada]
S --> M3[temas]
M1 --> B[migas de pan]
M2 --> B
M3 --> B
style U fill:#89b4fa,color:#11111b
style B fill:#a6e3a1,color:#11111b

Fíjate en la asimetría con [slug]. Un parámetro normal te da un valor atómico ya listo para usar; un catch-all te da una cadena estructurada que casi siempre querrás descomponer. Es más potente porque captura una jerarquía entera, y por eso mismo exige un paso extra de interpretación: la potencia y el trabajo de troceo van de la mano.

💡
La cola es una clave; resuélvela contra un mapa

El uso más potente de un catch-all no es pintar la cola, sino tratarla como clave para localizar el contenido que le corresponde. Una documentación cargada desde un CMS o desde una colección puede indexar sus entradas por su ruta completa —guia/instalacion— y, dentro de [...ruta].astro, buscar la entrada cuyo identificador coincida con Astro.params.ruta. La cola deja de ser texto decorativo y se convierte en la llave que abre exactamente un documento del árbol. Sin esa fuente que diga qué colas son válidas, un catch-all acepta cualquier cosa; con ella, se vuelve un enrutador de contenido preciso.

Enumerar en el build, y quién gana cuando varias rutas casan

En modo estático, un catch-all sigue necesitando getStaticPaths, con una particularidad: los valores que devuelves para el parámetro pueden contener barras, porque representan una cola de varios segmentos. Enumeras rutas completas, no tramos sueltos.

---
export async function getStaticPaths() {
  return [
    { params: { ruta: undefined } },        // la raiz, /docs
    { params: { ruta: 'guia' } },            // /docs/guia
    { params: { ruta: 'guia/instalacion' } },// /docs/guia/instalacion
  ];
}
---

Aquí surge una pregunta inevitable: si un catch-all casa con todo lo que cuelga de /docs, ¿qué pasa cuando también existe una ruta más específica, como src/pages/docs/guia.astro? Astro resuelve el conflicto con una regla de precedencia: lo más específico gana. Una ruta totalmente estática vence a una con un [param], y una con [param] vence a un [...rest]. El catch-all es la última red, el que atiende solo lo que ninguna ruta más concreta reclamó.

El orden de preferencia, de mayor a menor especificidad, es fácil de recordar:

  • Rutas totalmente estáticas —docs/guia.astro—, lo más concreto y lo primero en ganar.
  • Rutas con un segmento dinámico —docs/[pagina].astro—, en el medio.
  • Rutas catch-all —docs/[...ruta].astro—, la red final que recoge lo demás.

Con este orden puedes componer sin miedo lo general y lo particular en la misma carpeta: la ruta específica se antepone a su caso concreto, y el comodín recoge todo lo que nadie más reclamó. No compiten al azar; hay una jerarquía determinista que razonas de antemano.

📚

Documentacion anidada

Un arbol de docs de profundidad desconocida cabe en un solo fichero catch-all que resuelve la cola contra el contenido.

🍞

Migas de pan

Trocear la cola con split reconstruye el rastro de segmentos y alimenta una navegacion jerarquica sin esfuerzo.

🕸️

Pagina comodin

Un catch-all en la raiz atiende lo que ninguna otra ruta reclamo: ideal para un 404 rico o un proxy de contenido.

🪜

Precedencia clara

Estatico vence a param, y param vence a catch-all. El comodin solo recibe lo que nadie mas quiso.

Esa jerarquía de precedencia es lo que permite mezclar sin miedo lo general y lo particular. Puedes tener un [...ruta].astro cubriendo un árbol entero y, a la vez, un fichero estático para una página de ese árbol que merece un trato distinto: la específica se antepone, el resto cae en el comodín. No compiten al azar; hay un orden determinista que puedes razonar de antemano.

⚠️
Un catch-all en la raíz se lo come todo

Colocar src/pages/[...ruta].astro —un catch-all en la raíz de src/pages— es potentísimo y peligroso a partes iguales: casa con cualquier URL del sitio que no tenga una ruta más específica. Es la herramienta perfecta para un contenido cargado desde un CMS con URLs arbitrarias, pero si te descuidas puede tragarse rutas que creías cubiertas por otro fichero. Cuando lo uses en la raíz, ten siempre presente la regla de precedencia y comprueba que tus páginas específicas de verdad existen y ganan.

El catch-all delega la estructura de la URL en los datos

Los tres puntos hacen algo más radical de lo que aparentan: rompen el último vínculo rígido entre el árbol de ficheros y la forma de las URLs. Con rutas estáticas, la jerarquía de carpetas es la jerarquía de direcciones —lo vimos como un isomorfismo perfecto—. Con un [param], aflojas un tramo pero conservas la profundidad: sabes que hay exactamente un nivel variable. Con [...rest] renuncias incluso a la profundidad: el fichero ya no dicta cuántos niveles tiene la URL, solo dónde empieza la parte que delegas. Y esa profundidad, que el sistema de ficheros ha soltado, tiene que agarrarla otra cosa: tus datos. Un catch-all sin una fuente que le diga qué colas son válidas es una boca abierta que acepta cualquier cosa; su sentido nace de casarlo con un modelo —el árbol de una documentación, el mapa de un CMS, un grafo de páginas— que sí conoce la estructura real. Estás moviendo la definición de la jerarquía desde un lugar sintáctico y visible, las carpetas, hacia un lugar semántico y dinámico, los datos. Es la misma tensión que atraviesa todo este nivel, llevada a su extremo: cuanto más flexible es la ruta, menos dice el fichero y más tienen que decir los datos. El catch-all es el punto donde el enrutado por ficheros, esa idea de que la estructura visible es el significado, se da la vuelta y admite su límite: hay estructuras que no caben en un árbol de carpetas, y para ellas el fichero se aparta y deja que los datos gobiernen. Dominarlo es saber cuándo conviene esa entrega de poder y, sobre todo, no olvidar quién queda entonces a cargo de decir qué existe.

⚔️ Captura un árbol entero
  1. Crea src/pages/docs/[...ruta].astro con un getStaticPaths que enumere /docs, /docs/guia y /docs/guia/instalacion, incluyendo el caso ruta igual a undefined.
  2. Muestra Astro.params.ruta en crudo y observa cómo las URLs profundas llegan como una cadena con barras.
  3. Aplica split('/') y pinta unas migas de pan enlazando cada segmento con su prefijo acumulado.
  4. Añade un src/pages/docs/guia.astro estático y comprueba, por precedencia, que gana al catch-all para esa URL exacta.