Saltar al contenido

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.

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.

ASTRO
---// src/layouts/Base.astroimport { 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.

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.

ASTRO
<!-- 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.

HTML
<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.

JS
// astro.config.mjsimport { defineConfig } from 'astro/config';export default defineConfig({  prefetch: { prefetchAll: true, defaultStrategy: 'hover' },});
ASTRO
<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.