Rutas dinámicas y catch-all: corchetes como gramática
Los corchetes en el nombre de un fichero son un pequeño lenguaje: un par simple id captura un segmento dinámico, doble corchete id lo hace opcional, y corchetes con puntos slug capturan el resto de la ruta entera. Los paréntesis, en cambio, agrupan ficheros sin tocar la URL. Cada forma se traduce a una capacidad del router: parámetro, opcional, comodín o grupo sin ruta, y matchFilters valida que un parámetro tenga el formato correcto antes de coincidir.
Hasta ahora las rutas eran fijas: un fichero, una URL literal. Pero la mayoría de las aplicaciones necesitan rutas que se plieguen a un valor —el perfil de cualquier usuario, cualquier artículo del blog— sin crear un fichero por cada uno. SolidStart resuelve esto con una pequeña gramática de corchetes en los nombres de fichero: cada forma de corchete se traduce a una capacidad concreta del router. Aprender esa gramática es aprender a leer una carpeta routes/ como quien lee una tabla de rutas.
- Capturar un segmento variable con
[id]y leerlo conuseParams. - Absorber el resto de la ruta con un catch-all
[...slug]para docs, 404 o navegadores de ficheros. - Hacer un parámetro opcional con doble corchete
[[id]]. - Organizar ficheros sin afectar la URL con grupos entre paréntesis, y validar con
matchFilters.
Segmentos dinámicos con corchetes
Un par de corchetes en el nombre convierte ese segmento en un parámetro: routes/usuarios/[id].tsx responde a /usuarios/1, /usuarios/2 y cualquier otro valor, capturándolo bajo el nombre id. Dentro del componente lo lees con useParams, que devuelve un objeto reactivo. Puedes encadenar varios: usuarios/[id]/[pestana].tsx captura dos.
// src/routes/usuarios/[id].tsx -> /usuarios/:id
import { useParams } from "@solidjs/router";
export default function Usuario() {
const params = useParams();
return <h1>Usuario {params.id}</h1>;
}
Dos propiedades merecen destacarse. La primera es que una ruta literal siempre gana a una dinámica: si colocas usuarios/nuevo.tsx junto a usuarios/[id].tsx, la URL /usuarios/nuevo sirve la página literal, no la dinámica con id valiendo nuevo. Lo concreto vence a lo genérico, sin que ajustes prioridad alguna. La segunda es que, al navegar entre dos valores del mismo parámetro —de /usuarios/1 a /usuarios/2—, el componente no se remonta: es la misma plantilla de ruta, así que solo se recalcula lo que depende de params.id. Y nada te limita a un parámetro por ruta: usuarios/[id]/entradas/[entradaId].tsx captura dos segmentos dinámicos separados por uno fijo, y useParams te devuelve ambos en el mismo objeto.
Catch-all: absorber el resto de la ruta
Un catch-all antepone ... al nombre dentro de los corchetes —[...slug]— y captura cualquier número de segmentos restantes en un único parámetro, unidos por barras. Con routes/docs/[...ruta].tsx, una URL /docs/guia/router deja params.ruta valiendo guia/router. Es la herramienta para documentación jerárquica, navegadores de archivos y, sobre todo, para la página 404: un fichero routes/[...404].tsx en la raíz atrapa todo lo que ninguna otra ruta reclamó.
// src/routes/docs/[...ruta].tsx -> captura /docs/lo/que/sea
import { useParams } from "@solidjs/router";
export default function Docs() {
const params = useParams();
// /docs/guia/router -> params.ruta === "guia/router"
const partes = () => params.ruta.split("/");
return <article>Profundidad: {partes().length}</article>;
}
El catch-all es la red de seguridad del enrutado: como coincide con cualquier cosa, tiene la prioridad más baja, de modo que solo captura lo que ninguna ruta más específica reclamó. Colocado en la raíz como routes/[...404].tsx, se convierte en tu página de “no encontrado” sin configuración adicional; dentro de una carpeta, atrapa las profundidades de esa sección. Conceptualmente va siempre al final de su nivel: nada más específico puede vivir después de algo que lo absorbe todo.
// src/routes/[...404].tsx -> atrapa cualquier url no reclamada
import { HttpStatusCode } from "@solidjs/start";
export default function NoEncontrado() {
return (
<main>
<HttpStatusCode code={404} />
<h1>Página no encontrada</h1>
</main>
);
}
Opcionales con doble corchete
El doble corchete [[id]] hace el parámetro opcional: la ruta coincide tanto si el segmento está como si no. Un routes/usuarios/[[id]].tsx sirve a la vez /usuarios y /usuarios/1. Dentro, params.id estará definido o no según el caso, así que lo tratas con una comprobación. Es el patrón para vistas que muestran un listado cuando no hay selección y un detalle cuando lo hay, sin duplicar la ruta.
// src/routes/usuarios/[[id]].tsx -> /usuarios y /usuarios/1
import { Show } from "solid-js";
import { useParams } from "@solidjs/router";
export default function Usuarios() {
const params = useParams();
return (
<Show when={params.id} fallback={<ListaUsuarios />}>
<FichaUsuario id={params.id} />
</Show>
);
}
Conviene conocer su límite: la opcionalidad afecta solo al último segmento. Un parámetro opcional coincide con y sin ese tramo final, pero no habilita rutas más profundas tras él; a nivel de router equivale a un :id?, que cubre /usuarios y /usuarios/1 pero no /usuarios/1/editar. Para lo demás combinas piezas: un opcional para el último tramo, dinámicos para los intermedios, un catch-all para lo verdaderamente abierto.
flowchart TD
U[nombre de fichero en routes] --> D{que forma tiene}
D -->|texto literal| F[segmento fijo como blog]
D -->|corchete simple| DYN[parametro dinamico captura un valor]
D -->|doble corchete| OPT[parametro opcional con o sin valor]
D -->|corchete con puntos| CA[catch all absorbe el resto]
D -->|parentesis| G[grupo no aparece en la url]
style DYN fill:#89b4fa,color:#11111b
style CA fill:#f9e2af,color:#11111b
style G fill:#a6e3a1,color:#11111bGrupos con paréntesis: organizar sin tocar la URL
Los paréntesis hacen algo distinto de los corchetes: agrupan sin afectar la ruta. Una carpeta routes/(marketing)/ no aporta ningún segmento, de modo que routes/(marketing)/sobre/index.tsx sirve /sobre, no /marketing/sobre. Sirve para dos cosas: ordenar ficheros por área cuando el sistema de carpetas por sí solo no lo permite, y —combinado con un layout homónimo— aplicar una envoltura común a un conjunto de rutas sin meter un segmento artificial en la URL. La misma idea permite escapar de un layout: nombrar una carpeta como usuarios(detalle)/[id].tsx produce /usuarios/1 pero con un layout propio, fuera del de usuarios.
routes/
(marketing).tsx <- layout comun, sin segmento
(marketing)/
sobre.tsx <- /sobre
precios.tsx <- /precios
usuarios.tsx <- layout de /usuarios
usuarios/
index.tsx <- /usuarios
usuarios(detalle)/
[id].tsx <- /usuarios/1, con layout propio
Aquí (marketing).tsx envuelve /sobre y /precios con una cáscara común sin que la palabra marketing aparezca jamás en la barra de direcciones, mientras que usuarios(detalle)/ produce URLs bajo /usuarios pero deliberadamente fuera del layout de usuarios.tsx. Paréntesis para agrupar y compartir; paréntesis con nombre para escapar de la envoltura vecina.
Un [id] captura cualquier cosa, incluso basura. Con matchFilters del router restringes el formato: un RegExp para exigir dígitos, un array para un enum, o una función predicado. Si el valor no pasa el filtro, la ruta no coincide y cae al catch-all, típicamente tu 404. Así separas “no existe” de “formato inválido” sin escribir guardas dentro del componente.
import { Route } from "@solidjs/router";
const filtros = { id: /^\d+$/ }; // solo numeros
<Route path="/usuarios/:id" component={Usuario} matchFilters={filtros} />;[id] dinámico
Un segmento variable. [id] es :id; se lee con useParams. Encadenable en varios niveles.
[...slug] catch-all
Absorbe el resto de la ruta unido por barras. Ideal para docs jerárquicas y para la página 404.
[[id]] opcional
Coincide con y sin el segmento. Un listado que se vuelve detalle sin duplicar la ruta.
(grupo) sin URL
Los paréntesis organizan y comparten layout sin añadir segmento a la dirección.
No hay un fichero donde ordenes las rutas por prioridad: el orden emerge de su forma. De lo más específico a lo más ávido, una ruta literal gana a un [id], un [id] gana a un [[id]], y cualquiera de ellos gana a un [...slug], que siempre es el último recurso. Un matchFilters que rechaza empuja la coincidencia hacia la siguiente candidata en ese mismo orden. Diseñar la precedencia, por tanto, no es configurar nada: es elegir la forma correcta de cada nombre.
La lección profunda de este nivel es que SolidStart ha codificado toda la teoría de coincidencia de rutas en un puñado de caracteres en el nombre de un fichero, y que ese puñado es un lenguaje regular, no un montón de casos especiales que memorizar. Los corchetes hablan de captura: uno simple captura un segmento, uno doble lo captura opcionalmente, uno con puntos captura todos los que queden. Los paréntesis hablan de organización: agrupan ficheros y comparten layouts sin dejar huella en la URL. Y matchFilters habla de contrato: exige que lo capturado tenga la forma correcta antes de aceptar la coincidencia. Una vez que ves estas tres dimensiones —qué se captura, cómo se organiza, qué se valida— dejas de aprender reglas y empiezas a leer una carpeta routes/ como quien lee una gramática: cada nombre te dice exactamente qué URLs reclama y qué extrae de ellas. Esto tiene una consecuencia de diseño elegante: el orden de especificidad se vuelve intuitivo. Una ruta literal gana a un [id], un [id] gana a un [...slug], y un matchFilters que rechaza empuja la URL hacia la siguiente candidata. No hay un fichero de configuración de prioridades que ajustar; la prioridad emerge de la propia forma de los nombres, del más concreto al más ávido. Diseñar rutas deja de ser configurar un router y pasa a ser escribir en este lenguaje: eliges los corchetes por lo que capturan, los paréntesis por lo que ocultan, y los filtros por lo que garantizan, y la carpeta resultante es a la vez la implementación y su documentación.
- Crea
routes/usuarios/[id].tsx, léelo conuseParamsy visita varios ids para ver el mismo componente con distinto valor. - Añade
routes/docs/[...ruta].tsx, navega a/docs/a/b/cy comprueba queparams.rutaesa/b/c; parte el string por barras. - Convierte el usuario en
[[id]].tsxy confirma que responde tanto a/usuarioscomo a/usuarios/1, ramificando segúnparams.id. - Añade un
matchFilterscon/^\d+$/alidy verifica que/usuarios/abccae al catch-all en vez de coincidir. - Mete un grupo
(marketing)con una página dentro y comprueba que su URL omite el nombre del grupo.