Templates

Go html/template with HTMX partials and template composition.

Go’s html/template package handles rendering with auto-escaping, composition, and zero dependencies. Combined with HTMX, your templates pull double duty — serving full pages and HTML fragments.

Template Composition

Define a base layout and override blocks per page:

layouts/layout.html:

{{ define "layout" }}
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="utf-8">
  <title>My App</title>
  <script src="https://unpkg.com/htmx.org@2.0.4"></script>
  <link href="/static/styles.css" rel="stylesheet">
  {{ block "head" . }}{{ end }}
</head>
<body hx-boost="true">
  {{ template "nav" . }}
  <main>
    {{ block "content" . }}{{ end }}
  </main>
</body>
</html>
{{ end }}

pages/lobby.html:

{{ define "lobby" }}
  {{ template "layout" . }}
{{ end }}

{{ define "content" }}
<div class="container mx-auto px-4">
  <h1>Game Lobby</h1>
  <span class="font-mono text-3xl">{{ .State.Game.Slug }}</span>
  <!-- content here -->
</div>
{{ end }}

The page defines which layout to use, then overrides the content block.

HTMX Partials

The same template that renders in a full page can be returned as a fragment for HTMX:

// Full page render
func (h *Handler) GamePage(w http.ResponseWriter, r *http.Request) {
    data := h.getGameData(r)
    h.tmpl.ExecuteTemplate(w, "game", data)
}

// HTMX partial — just the scoreboard
func (h *Handler) Scoreboard(w http.ResponseWriter, r *http.Request) {
    data := h.getGameData(r)
    h.tmpl.ExecuteTemplate(w, "scoreboard", data)
}
<!-- In the page template -->
<div hx-get="/game/{{ .ID }}/scoreboard" hx-trigger="sse:refresh" hx-swap="innerHTML">
  {{ template "scoreboard" . }}
</div>

SSE-Driven Updates

Combine HTMX’s SSE extension with server-sent events for real-time updates:

<div
  hx-ext="sse"
  sse-connect="/game/{{ .State.Game.ID }}/events"
  hx-get="/game/{{ .State.Game.ID }}"
  hx-trigger="sse:refresh"
  hx-target="main"
  hx-swap="innerHTML"
>
  <!-- This entire div re-renders when the server sends a "refresh" event -->
</div>

The server broadcasts event: refresh\ndata: player-joined via SSE, HTMX catches it, fetches the updated HTML, and swaps it in. No JavaScript written.

Template Data

Pass structured data to templates using a view model:

type GamePageData struct {
    State  *service.GameState
    User   *model.User        // nil if not logged in
    Slots  []SlotData
}

func (h *Handler) GamePage(w http.ResponseWriter, r *http.Request) {
    user, _ := r.Context().Value(middleware.UserContextKey).(*model.User)
    state, _ := h.gameSvc.GetGameState(r.Context(), gameID)

    h.tmpl.ExecuteTemplate(w, "game", GamePageData{
        State: &state,
        User:  user,
        Slots: buildSlots(state),
    })
}

Template Functions

Add custom functions for common formatting:

tmpl := template.New("").Funcs(template.FuncMap{
    "formatScore": func(n int32) string { return fmt.Sprintf("%d", n) },
    "timeAgo":     timeAgo,
    "safeHTML":    func(s string) template.HTML { return template.HTML(s) },
})

Gotchas

  • Auto-escaping is on by default. This is good. Don’t use safeHTML unless you trust the content.
  • Template names matter. {{ define "content" }} and {{ block "content" . }} must match exactly.
  • Pass the dot. {{ template "nav" . }} — forgetting the . means your partial gets no data.
  • Parse all templates together. Use template.ParseGlob("templates/**/*.html") or parse them in the right order so definitions resolve.