El ClientRouter: navegar como una SPA sin dejar de ser un MPA
El componente que intercepta la navegación entre páginas y la reemplaza por un intercambio de DOM sin recarga. Qué es astro:transitions, dónde se coloca el ClientRouter, qué hace exactamente al interceptar un enlace, en qué se distingue de un router de SPA, cómo encaja en la mejora progresiva y qué fallback ofrece a los navegadores sin la API nativa.
Astro es, en su médula, un framework multipágina: cada ruta es un documento HTML independiente y moverse entre ellas significa que el navegador tira el documento actual, pide otro y lo pinta de cero. Es robusto y es la web de siempre, pero tiene un precio visible —el destello en blanco, el salto al inicio, el estado que se pierde, los recursos compartidos que se vuelven a descargar—. El <ClientRouter /> es la pieza que borra ese precio sin cambiar el modelo: sigue habiendo un documento por ruta renderizado en el servidor, pero la transición entre ellos deja de ser una recarga y pasa a ser un intercambio quirúrgico del DOM. El resultado se siente como una SPA sin serlo.
- Activar el
<ClientRouter />importándolo deastro:transitionsy colocándolo en el<head>de un layout compartido. - Entender qué intercepta y qué hace exactamente al sustituir la recarga por un intercambio de DOM.
- Situar la mejora dentro de la mejora progresiva y distinguirla de un router de SPA.
- Controlar excepciones con
data-astro-reload,data-astro-historyy el atributofallback.
De la recarga completa al intercambio intervenido
En la navegación multipágina clásica, seguir un enlace es un acto destructivo: el navegador descarta por completo el documento vivo —su árbol DOM, sus scripts en marcha, su estado en memoria— y construye otro desde el HTML que recibe. Es simple y a prueba de balas, pero cada salto reinicia el mundo. El <ClientRouter /> se interpone en ese acto. Se importa de astro:transitions y se coloca una sola vez, en el <head>, idealmente dentro del layout que comparten todas tus páginas para que la mejora cubra el sitio entero.
---
// src/layouts/Base.astro
import { ClientRouter } from 'astro:transitions';
---
<html lang="es">
<head>
<meta charset="utf-8" />
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
Con esa única línea en el sitio, los enlaces internos dejan de provocar recargas. No hay que reescribir un solo <a>: los mismos anclas nativos que ya navegaban empiezan a navegar de otra manera. Esa es la clave de diseño —la mejora es aditiva y no invasiva—, y explica por qué el componente vive en el head y no envuelve tu contenido: no es un contenedor, es un interceptor global.
Qué intercepta y qué hace en su lugar
Cuando el <ClientRouter /> está presente, un pequeño script escucha tres cosas: los clics en enlaces del mismo origen, los envíos de formularios y los botones de atrás y adelante del navegador. En lugar de dejar que el navegador recargue, toma el control: pide en segundo plano el HTML de la ruta destino, lo interpreta, fusiona su <head> con el actual, e intercambia el <body> por el nuevo. Todo ese trasplante ocurre dentro de document.startViewTransition cuando el navegador la soporta, de modo que el cambio no es un corte seco sino una transición animada, y de ahí el nombre de la familia: view transitions.
El cambio no es solo estético. Al no recargar, la conexión, los recursos ya cacheados y el propio runtime del navegador se conservan; solo viaja por la red el HTML de la página nueva. Astro además gestiona el foco y el desplazamiento como lo haría una navegación real —vuelve arriba en un salto normal, restaura la posición al ir atrás— para que la experiencia sea indistinguible de la nativa salvo por la fluidez añadida.
Merece detenerse en la fusión del <head>, porque ahí vive una sutileza que explica mucho de lo que viene después. Astro no descarta el head viejo para colocar el nuevo entero: los compara. Los estilos y scripts que ya estaban y siguen haciendo falta se conservan sin volver a descargarse ni re-ejecutarse; lo que solo aparece en la página nueva se añade; lo que solo vivía en la vieja se retira. Ese diffing es lo que evita el parpadeo de estilos y la re-descarga de lo compartido —y es también la raíz de una regla importante: como los scripts que persisten no se vuelven a ejecutar, el código que debe correr en cada página no podrá confiar en la recarga y tendrá que engancharse a un evento del ciclo de navegación, algo que estudiaremos en detalle más adelante.
Hay, por último, un matiz de accesibilidad que la navegación intervenida hereda de las SPAs y conviene tener presente desde ya. Cuando el navegador recarga de verdad, anuncia el cambio de página a las tecnologías de asistencia sin que hagas nada; cuando el cambio es un intercambio de DOM, ese anuncio no sucede solo. Astro cubre lo esencial moviendo el foco y actualizando el título, pero en interfaces exigentes tendrás que reforzar el aviso del cambio de ruta. Interceptar la navegación traslada a tus manos una responsabilidad que la recarga asumía por ti, y hacerlo bien es parte del oficio.
Conviene subrayar lo que no cambia. Cada página que sirve el <ClientRouter /> es el mismo HTML que Astro habría generado sin él: renderizado en el servidor o en el build, con su SEO intacto, su contenido completo en la primera carga y cero dependencia de que exista JavaScript de cliente para tener contenido. El router no genera las páginas en el cliente; solo intercepta cómo se llega de una a otra. Sigues teniendo un sitio de documentos, no una aplicación que se pinta en el navegador.
Un MPA que se siente como una SPA, sin volverse una
La confusión más común es leer el <ClientRouter /> como el router de una SPA, y no lo es. Un router de aplicación de página única posee el enrutado: mantiene el estado del sitio en memoria, decide en el cliente qué componente montar para cada URL y exige que la app entera —o buena parte— se hidrate y viva en el navegador. El coste es un bundle grande y un modelo mental centrado en el cliente. El <ClientRouter /> invierte esa relación: el servidor sigue siendo la fuente de verdad de cada ruta, el cliente no posee estado de enrutado, y el añadido es un script diminuto cuyo único trabajo es traer el siguiente documento y coserlo en el sitio del actual. No es una app que finge ser páginas; son páginas que fingen ser una app.
Esta distinción tiene consecuencias prácticas felices. No hay que sincronizar un estado de cliente con la URL, no hay hidratación masiva que retrase la interactividad, no hay divergencia entre lo que renderiza el servidor y lo que reconstruye el cliente. Ganas la fluidez de la SPA —transiciones sin destello, estado que puede persistir, sensación de instantaneidad— sin adoptar su arquitectura ni pagar su factura de JavaScript. Y como el runtime del navegador no se destruye en cada salto, tampoco pagas el coste oculto de las SPAs: no hay memoria que crezca sin control salto tras salto, ni un árbol de componentes que rehidratar; cada página parte de un HTML fresco servido por Astro, y el estado que sobrevive lo hace por elección explícita, no por accidente.
Mejora progresiva: fallback, exclusiones y anticipación
El <ClientRouter /> es un ejemplo de manual de mejora progresiva: la base funciona sin él y él solo añade una capa que se puede quitar sin romper nada. Si el usuario tiene JavaScript deshabilitado, los enlaces vuelven a ser recargas completas y el sitio funciona igual, solo que sin la animación. Si el navegador no implementa la API nativa de view transitions, Astro no se rinde: aplica su propia simulación de la transición. Ese comportamiento se gobierna con el atributo fallback, que admite tres valores.
<!-- animate: Astro simula la transicion en navegadores sin la API nativa (por defecto) -->
<ClientRouter fallback="animate" />
<!-- swap: intercambia el DOM sin animar donde no hay API nativa -->
<ClientRouter fallback="swap" />
<!-- none: sin API nativa, se recae en la recarga completa clasica -->
<ClientRouter fallback="none" />
Por defecto vale animate, así que la fluidez se percibe incluso donde el navegador aún no trae la API. Rara vez tendrás que tocarlo; conocerlo importa para razonar sobre qué verá cada visitante según su navegador, no para configurarlo a diario.
A veces, en cambio, no quieres interceptar. Un enlace que sale hacia otra aplicación de tu dominio, o hacia una página con scripts que exigen un arranque limpio, conviene que provoque una recarga real. Para eso está data-astro-reload: marca ese ancla concreto y el router lo deja pasar como navegación completa. Y cuando un salto no debería crear una entrada nueva en el historial —el paso siguiente de un asistente, una redirección que no quieres que el botón atrás repita— data-astro-history="replace" sustituye la entrada actual en vez de apilar una nueva.
<a href="/panel-legacy" data-astro-reload>Panel (recarga completa)</a>
<a href="/asistente/paso-2" data-astro-history="replace">Continuar</a>
Estos dos atributos son el nivel de control más fino que necesitarás la mayoría de las veces: uno decide si se intercepta, el otro decide cómo se recuerda el salto. Con ellos cubres los pocos casos en que la navegación intervenida no es lo que quieres, sin renunciar a ella en todo el resto.
La cara opuesta de excluir un enlace es anticiparlo. El <ClientRouter /> se entiende de maravilla con el prefetch de Astro: cuando está activo, el sitio puede precargar en segundo plano el HTML de una ruta antes de que la pidas —al pasar el ratón por encima del enlace o cuando este asoma en el viewport—, de modo que al hacer clic la página ya está esperando y la transición se siente instantánea. Se habilita en la configuración y se afina enlace a enlace con data-astro-prefetch.
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
prefetch: { prefetchAll: true, defaultStrategy: 'hover' },
});
<a href="/blog" data-astro-prefetch>Blog (se precarga al acercar el ratón)</a>
Interceptar, excluir y anticipar son las tres palancas con las que gobiernas la navegación: la primera es el comportamiento por defecto que instala el router, la segunda lo desactiva justo donde estorba, y la tercera lo acelera justo donde importa. Con ellas, la misma capa que hizo fluida la navegación te deja moldearla ruta a ruta sin volver a tocar la arquitectura del sitio.
flowchart TD A[usuario hace clic en un enlace interno] --> B[ClientRouter intercepta el clic] B --> C[fetch del HTML de la ruta destino] C --> D[fusiona el head y prepara el nuevo body] D --> E[startViewTransition del navegador] E --> F[intercambio del DOM sin recargar] F --> G[foco y scroll gestionados] G --> H[astro page-load] style A fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style H fill:#a6e3a1,color:#11111b
Una linea, todo el sitio
Importa ClientRouter de astro:transitions y ponlo en el head de un layout compartido. No hay que tocar los enlaces.
Intercambio, no recarga
Intercepta clics, formularios y atras/adelante; trae el HTML destino y cambia el body dentro de una view transition.
MPA que parece SPA
El servidor sigue renderizando cada ruta; el anadido es un script diminuto, sin router de cliente ni hidratacion masiva.
Fallback y excepciones
fallback anima o recae en recarga donde no hay API nativa; data-astro-reload y data-astro-history afinan enlaces sueltos.
Durante quince años la industria asumió un trueque: si querías que tu sitio se sintiera instantáneo al navegar, tenías que convertirlo en una aplicación de página única —mover el enrutado al cliente, hidratar todo, mantener el estado del mundo en memoria del navegador— y aceptar a cambio bundles enormes, primeras cargas lentas y una fragilidad nueva, la de un sitio que sin JavaScript no es nada. El <ClientRouter /> desmonta ese trueque al observar que era falso. La lentitud de la navegación multipágina no venía de ser multipágina, sino de recargar: de descartar el documento vivo y reconstruir el runtime en cada salto. Ataca exactamente ese punto y solo ese. Deja intacto todo lo que hacía valiosa la arquitectura de documentos —contenido completo en el servidor, SEO sin esfuerzo, funcionamiento sin cliente, un bundle mínimo— y elimina su único defecto perceptible interceptando la navegación para intercambiar el DOM en vez de rehacerlo. La lección es más grande que el componente: muchos de los males que atribuimos a una arquitectura son en realidad de un detalle de implementación, y confundirlos nos lleva a reescrituras totales cuando bastaba una intervención quirúrgica. Astro no te pide elegir entre la solidez del documento y la fluidez de la app; te demuestra que esa elección nunca fue necesaria, y que la mejor forma de acelerar la web suele ser quitarle lo que sobra, no añadirle otra capa que lo compense.
- Importa
ClientRouterdeastro:transitionsy colócalo en el<head>de tu layout base; navega entre dos páginas y observa que desaparece el destello de recarga. - Abre las herramientas de red del navegador y confirma que al saltar solo viaja el HTML de la página nueva, no todos los recursos compartidos.
- Marca un enlace con
data-astro-reloady comprueba que ese salto sí provoca una recarga completa, a diferencia del resto. - Cambia el
fallbackanone, simula un navegador sin la API nativa y razona qué experiencia recibiría ese visitante frente alanimatepor defecto.