htmx: Build Interactive Web Apps with HTML · Production Concerns

URLs, History and hx-boost

Make htmx pages shareable and reload-safe: push URLs, boost navigation, return full pages on direct visits, and set Vary for caches.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Contact detail navigation where clicking a contact loads it into the page and changes the URL to /contacts/2. Reloading or sharing that URL shows the same contact, and the Back button works.

The idea

  • hx-push-url="true" puts the request URL in the address bar and adds a history entry.
  • hx-boost="true" turns normal links and forms into htmx requests that swap the <body> and push the URL. Links still work without JavaScript.
  • The golden rule: every URL you push must also work as a normal page load. A reload, a shared link, a history-cache miss or a search engine requests the URL directly. Check HX-Request to return a fragment to htmx and a full page otherwise, and send Vary: HX-Request.
  • htmx 2 restores a history-cache miss with HX-Request: true and HX-History-Restore-Request: true; treat the second header as "send the full page".

The code

Pushed URLs and a server that knows the differenceTry it yourself
<nav hx-target="#detail" hx-push-url="true">
  <button hx-get="/contacts/1">Ada</button>
  <button hx-get="/contacts/2">Grace</button>
</nav>
<section id="detail">Choose a contact.</section>
<button id="direct">What would a direct visit to /contacts/2 get?</button>
<pre id="raw" style="white-space:pre-wrap;font-size:12px"></pre>
<script>
  // A reload or shared link is a plain request without htmx's headers. Imitate one:
  document.getElementById("direct").addEventListener("click", () => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", "/contacts/2");
    xhr.onload = () => { document.getElementById("raw").textContent = xhr.responseText; };
    xhr.send();
  });
</script>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const people = { 1: ["Ada Lovelace", "Mathematician"], 2: ["Grace Hopper", "Computer scientist"] };
  server.get("/contacts/:id", (req) => {
    const [name, role] = people[req.params.id];
    const fragment = `<h3>${name}</h3><p>${role}</p>`;
    const isHtmx = req.headers["hx-request"] === "true" && !req.headers["hx-history-restore-request"];
    return { headers: { Vary: "HX-Request" },
             body: isHtmx ? fragment : `<!doctype html><html><body><nav>...</nav><section id="detail">${fragment}</section></body></html>` };
  });
</script>

(The last button sends a plain request without htmx's headers, like a reload or a shared link, and shows the response as text: the full page.)

Common mistakes

  • Pushing URLs that return only a fragment: reloading shows an unstyled scrap of HTML.
  • Missing Vary: HX-Request: a browser or CDN cache serves the fragment to a normal visit, or the full page to htmx.
  • hx-push-url on every small interaction (tabs, filters as you type): use hx-replace-url or no URL change.
  • Boosting a page whose scripts expect a fresh page load each time: initialise widgets on htmx:load instead.

Key points

  • Pushed URLs must render as full pages on direct requests.
  • Decide by HX-Request (and HX-History-Restore-Request), and send Vary: HX-Request.
  • hx-boost is progressive enhancement for normal links and forms.

Further reading: hx-push-url · hx-boost · htmx docs: history · MDN: Vary

Practice questions

A user reloads /contacts/2 after htmx pushed it, and sees an unstyled fragment. What's wrong?

Answer

The server returns the fragment for every request. It must return the full page when HX-Request is absent (or HX-History-Restore-Request is present).

Why send Vary: HX-Request?

Answer

The same URL returns different bodies depending on that header; Vary tells caches to store them separately.

What does hx-boost do, and why is it progressive enhancement?

Answer

It turns normal links and forms into htmx requests that swap the body and push the URL; without JavaScript they still work as normal links and forms.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?