posts / go

Adventures in Go and HTMX - Part 2

Introduction - Boosting

In Part 1 of this series we intentionally built the game using the most boring navigation primitive on the web: plain old anchor tags. When the player clicks “Go North,” the browser performs a full navigation. It tears down the current document, requests /room/atrium, downloads a complete HTML page, parses it, rebuilds the DOM, re-evaluates styles, and only then shows the next room. That “hard navigation” is robust and simple and it’s also why MPAs (Multi Page Applications) can feel heavy.

This article is about a very specific kind of upgrade. Keep everything you like about server-rendered HTML,the server is the source of truth, routes are URLs, links are links, but make navigation behave more like a single page app. Not by introducing a client-side router or moving state into the browser, but by having the browser request the next page and then swap the response into the current document.

That is exactly what HTMX’s hx-boost attribute does. The HTMX docs describe it as a way to “boost” normal anchors and form tags so they use AJAX, while preserving progressive enhancement, that is, if JavaScript isn’t available, they still work as standard links and forms. In practice, for links, boosting means clicking an <a href="..."> issues an AJAX GET to the link’s href, then replaces the <body> contents with the response using innerHTML swap, and pushes a new history entry so Back/Forward keeps behaving like the web.

Two important consequences fall out of this:

We don’t rewrite our handlers

Our Go server can keep returning full HTML pages for /room/{id} exactly like part 1. Boosting is a client-side “transport” optimization.

We keep writing real HTML

We continue to use <a href="..."> and later <form action="...">. HTMX doesn’t require us to map out a complex client-side router or hijack clicks based on generic CSS classes. Instead, it respects the HTML we already wrote.

Under the hood, HTMX listens for clicks on these boosted elements, prevents the browser’s default hard navigation, and uses the existing href attribute as the single source of truth for its AJAX request. Because the routing logic remains completely defined by the HTML element itself, the code remains incredibly easy to reason about. And as a bonus for progressive enhancement, if JavaScript is disabled, the browser just executes a standard page load and the text adventure keeps running without missing a beat.

And if you want a simple way to verify what’s happening, HTMX makes it easy. When a request comes from a boosted link or form, it includes the HX-Boosted requests header. That detail will become crucial later when we start returning partial fragments instead of full pages. For now, it’s simply a neat confirmation that the “magic” is just standard HTTP.

Install HTMX

Open templates/room.html and add this script tag inside <head>. These are official snippet from the HTMX docs:

<script
  src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
  integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
  crossorigin="anonymous"
></script>

HTMX is dependency free and intended to be used directly in the browser, so you don’t need a bundler to get started. Just include the script.

Small production note: CDNs are convenient for learning, but you may want to self host in production.

For our local text adventure, CDN is perfect.

Turn On Boosting

Now add hx-boost="true" to the <body> tag.

While we’re editing the template, let’s also make the <title> include the room name. This will become a simple “proof” that navigation is working correctly without full reloads.

Here’s the full updated templates/room.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>{{.Room.Name}} · Adventures in HTMX and Go</title>
    <style>
      body {
        font-family: sans-serif;
        max-width: 700px;
        margin: 2rem auto;
        padding: 1rem;
      }
      .room {
        border: 2px solid #333;
        padding: 20px;
        border-radius: 8px;
      }
      .exits {
        list-style: none;
        padding: 0;
      }
      .exits li {
        margin: 10px 0;
      }
      a {
        background: #eee;
        text-decoration: none;
        padding: 5px 10px;
        border: 1px solid #ccc;
        color: black;
      }
      a:hover {
        background: #ddd;
      }
    </style>

    <script
      src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
      integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
      crossorigin="anonymous"
    ></script>
  </head>

  <body hx-boost="true">
    <div class="room">
      <h1>{{.Room.Name}}</h1>
      <p>{{.Room.Description}}</p>

      <h2>Exits</h2>
      <ul class="exits">
        {{range .Room.Exits}}
        <li>
          <a href="/room/{{.To}}">{{.Label}}</a>
        </li>
        {{else}}
        <li>(There are no exits. You are trapped.)</li>
        {{end}}
      </ul>
    </div>
  </body>
</html>

What hx-boost="true" actually does here?

Because hx-boost is inherited, putting it on <body> means:

  • Every link inside the body is now a “boosted” link.
  • Clicking it triggers an AJAX. request instead of a full navigation.
  • The response swaps into the <body> (default target).
  • The URL is pushed into history (for anchors).

Your server is still returning full HTML pages. HTMX is just doing the “navigation” part more cleverly.

Run It And Feel The Difference

Start you server the same way you did in part 1:

go run .

(or air if you’re using it)

Now open http://localhost:4040 and click Go North and Go South a few times. You should notice the URL changes normally (/room/hallway <-> /room/atrium). There is no full white page flash. Back/Forward browser buttons work like correctly.

Prove It

Open your browser devtools->Network.

Click Go North again.

You should see a request that looks like:

  • Type: XHR / fetch (not “document”)
  • Method: Get
  • Request Headers: includes HX-Boosted (and other HX-* headers)

That HX-Boosted header is HTMX politely whispering to your Go server: “FYI, I intercepted a normal link and turned it into AJAX.” We won’t use that header yet, but later, it becomes incredibly useful for deciding whether to return a full page or a fragment.

Gotchas

Boosting is simple, but a few behaviors are worth knowing:

HTMX swaps using innerHTML into the <body> tag for boosted navigation. This is why it feels like “navigation” rather than “update one div.”

Title updates are a thing

By default, HTMX will update the document title if it finds a <title> tag in the response content. That’s why we changed the template title to include {{.Room.Name}}.

Scroll behavior changes

For boosted links/forms, the default behavior is to scroll to the top (show:top). In our tiny game this is totally fine. Later, when we have a scrolling game log, we’ll control scrolling explicitly.

Forms behave slightly differently

Boosting forms uses AJAX too, but by default it does not push a new history entry for the form action. You can opt in with hx-push-url which we will use later.

And this is a perfect setup for part 3, where we introduce command input.

Code

You can find full code on GitHub.

RS
Rob Sliwa

Coder | Book Lover | Lifelong Learner

PT
Pawan Tripathi

Writes about infrastructure, agentic coding, and trying to keep things small.