Historial y boost
Historial y boost
Si tu aplicación cambia contenido sin cambiar la URL, rompes el botón “atrás”, los marcadores y la posibilidad de compartir un enlace. htmx tiene dos piezas para eso: el manejo del historial y el boost de la navegación clásica.
hx-push-url: escribir en la barra de direcciones
<a hx-get="/tareas" hx-target="#contenido" hx-push-url="true">Tareas</a>
La petición es AJAX, pero la URL del navegador pasa a /tareas y queda una entrada en el historial.
También acepta una URL explícita, útil cuando el endpoint del fragmento no es la URL que quieres mostrar:
<button hx-get="/api/fragmentos/tareas"
hx-target="#contenido"
hx-push-url="/tareas">
Tareas
</button>
hx-replace-url: sin nueva entrada
Reemplaza la URL actual en lugar de añadir una entrada. Es lo correcto para filtros y búsquedas, donde no quieres que el usuario tenga que pulsar “atrás” veinte veces:
<input type="search"
name="q"
hx-get="/buscar"
hx-trigger="input changed delay:400ms"
hx-target="#resultados"
hx-replace-url="true">
flowchart LR
subgraph P["hx-push-url"]
A["/inicio"] --> B["/tareas"] --> C["/tareas/5"]
C -.->|atrás| B -.->|atrás| A
end
subgraph R["hx-replace-url"]
D["/buscar?q=ca"] -.->|reemplaza| E["/buscar?q=caf"] -.->|reemplaza| F["/buscar?q=cafe"]
end
También desde el servidor:
headers: { "HX-Push-Url": "/tareas/5" }
headers: { "HX-Replace-Url": "/buscar?q=cafe" }
El cambio grande de htmx 4: sin caché de DOM
htmx 2 guardaba snapshots del DOM en localStorage. Al pulsar “atrás”, restauraba el HTML
guardado sin ir a la red. Sonaba bien y en la práctica traía tres problemas: el límite de tamaño de
localStorage, snapshots obsoletos que mostraban datos viejos y contenido sensible persistido en
el navegador.
htmx 4 lo eliminó: al navegar hacia atrás, vuelve a pedir el contenido al servidor.
| Aspecto | htmx 2 | htmx 4 |
|---|---|---|
| Al pulsar “atrás” | Restaura desde localStorage | Petición de red |
| Datos obsoletos | Posibles | No |
| Contenido sensible en disco | Sí | No |
| Sin conexión | Funciona | Falla |
| Complejidad del core | Alta | Baja |
Configuración:
htmx.config.history = true; // por defecto: refetch
htmx.config.history = "reload"; // recarga completa de la página
htmx.config.history = false; // desactiva el manejo del historial
Si necesitas el comportamiento anterior, existe la extensión hx-history-cache, que restaura
las páginas desde sessionStorage:
<script src="/js/ext/hx-history-cache.js"></script>
<meta name="htmx-config" content='extensions:"hx-history-cache"'>
sessionStorage en lugar de localStorage acota el problema del contenido sensible: se borra al
cerrar la pestaña.
Qué implica en el servidor
Como el “atrás” es una petición real, tu servidor debe poder responder la URL completa. El header
HX-History-Restore-Request: true te dice que es una restauración:
if (req.headers.get("HX-History-Restore-Request") === "true") {
// Devuelve la página completa: htmx va a reemplazar el elemento de historial
return html(paginaCompleta(contenido));
}
Este es el argumento definitivo para el patrón “una ruta, dos respuestas” del capítulo 3: si cada URL sabe servirse completa, el historial funciona solo.
hx-history-elt: qué parte se restaura
Por defecto, htmx restaura el <body>. Si tu layout tiene una cabecera y una barra lateral que
nunca cambian, marca el contenedor real:
<body>
<nav>…</nav>
<main id="contenido" hx-history-elt>…</main>
</body>
Un solo elemento por documento debe tener hx-history-elt.
hx-boost: AJAX para enlaces y formularios
hx-boost convierte los <a> y <form> normales en peticiones htmx, conservando el HTML
semántico:
<div hx-boost:inherited="true">
<a href="/tareas">Tareas</a>
<a href="/perfil">Perfil</a>
<form action="/buscar" method="get">
<input name="q">
<button>Buscar</button>
</form>
</div>
Qué hace exactamente:
- Intercepta el clic y el submit.
- Hace la petición por AJAX.
- Reemplaza el
<body>(o elhx-targetque definas). - Empuja la URL al historial automáticamente.
Y qué pasa sin JavaScript: los enlaces y formularios funcionan como siempre. Esa es la gracia: degradación elegante gratis.
<body hx-boost:inherited="true" hx-target:inherited="#contenido">
<nav>
<a href="/">Inicio</a>
<a href="/tareas">Tareas</a>
</nav>
<main id="contenido">…</main>
</body>
Con esas dos líneas tienes una aplicación que navega sin recargas, con URLs reales, historial funcionando e indexable por buscadores.
Excluir elementos del boost
<div hx-boost:inherited="true">
<a href="/tareas">Interno (boosted)</a>
<a href="/informe.pdf" hx-boost="false">Descargar PDF</a>
<a href="https://otro-sitio.com" hx-boost="false">Enlace externo</a>
</div>
Los enlaces con target="_blank", download o hacia otro origen no se boostean automáticamente,
pero conviene ser explícito.
El servidor con boost
Toda petición boosteada lleva HX-Boosted: true. Como el target suele ser un contenedor grande,
el patrón es:
function responder(req, contenido, titulo) {
const esHtmx = req.headers.get("HX-Request") === "true";
if (esHtmx) {
// Fragmento + título para que htmx actualice el <title>
return html(`<title>${titulo}</title>${contenido}`);
}
return html(layout(contenido, titulo));
}
htmx toma el <title> de la respuesta y actualiza el de la página, salvo que uses
hx-swap="… ignoreTitle:true".
Precargar la navegación
La extensión hx-preload carga el contenido al pasar el mouse por encima del enlace, de modo que
al hacer clic ya está en caché:
<script src="/js/ext/hx-preload.js"></script>
<meta name="htmx-config" content='extensions:"hx-preload"'>
<a href="/tareas" hx-preload>Tareas</a>
La sensación de instantaneidad es notable y el costo es una petición extra por enlace visitado con el mouse. Úsalo en navegación principal, no en listas de cien elementos.
Restauración del scroll
htmx 4 usa la Navigation API del navegador para restaurar la posición del scroll al volver atrás. Funciona sin configuración.
Si necesitas control manual sobre el scroll de un swap concreto, están los modificadores del capítulo 6:
<div hx-get="/pagina/2" hx-swap="innerHTML show:top"></div>
Ejemplo: navegación completa
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Panel</title>
<meta name="htmx-config" content='extensions:"hx-preload"'>
<script src="/js/htmx.min.js"></script>
<script src="/js/ext/hx-preload.js"></script>
</head>
<body hx-boost:inherited="true"
hx-target:inherited="#contenido"
hx-swap:inherited="innerHTML show:top"
hx-indicator:inherited="#barra">
<div id="barra" class="htmx-indicator"></div>
<nav>
<a href="/" hx-preload>Inicio</a>
<a href="/tareas" hx-preload>Tareas</a>
<a href="/informes.pdf" hx-boost="false">Informe PDF</a>
</nav>
<main id="contenido" hx-history-elt>
<!-- contenido de la página -->
</main>
</body>
</html>
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El botón “atrás” muestra una página rota | El servidor no sabe servir esa URL completa | Implementar “una ruta, dos respuestas” |
| Cada tecleo deja una entrada en el historial | Se usó hx-push-url en una búsqueda | Usar hx-replace-url |
El PDF se abre dentro del <main> | Está boosteado | hx-boost="false" |
El <title> no cambia al navegar | La respuesta no incluye <title> | Incluirlo en el fragmento |
| Se restaura el layout completo dentro del layout | Falta hx-history-elt | Marcar el contenedor real |
| Se perdió el caché de historial al migrar | Cambio de htmx 4 | Extensión hx-history-cache |
Resumen
hx-push-urlañade entradas al historial;hx-replace-urlreemplaza la actual (filtros).- htmx 4 eliminó la caché de DOM: al volver atrás se vuelve a pedir el contenido a la red.
htmx.config.historyaceptatrue,falseo"reload";hx-history-cacherestaura el comportamiento antiguo consessionStorage.hx-history-eltmarca qué contenedor se restaura.hx-boost:inherited="true"convierte toda la navegación en AJAX conservando la degradación elegante.hx-preloadprecarga al pasar el mouse.- El patrón “una ruta, dos respuestas” es lo que hace que todo esto funcione.
Siguiente: Streaming: SSE, WebSockets y multipart →.