Templates
Use Go templates to render full pages and HTML fragments.
Templates are the center of the GO(A)HT UI model. HTML fragments are the primary UI API, and full pages are just larger compositions of those same fragments.
Template Roles
Keep templates in three groups:
layouts/for the outer document shellpages/for full-page compositionspartials/for reusable fragments and HTMX swap targets
That split makes the render contract obvious:
- page handlers execute a page template
- partial handlers execute a fragment template
Full Page vs Partial
A full page returns the whole document, usually through a layout:
{{ define "layouts/app.html" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ .Title }}</title>
<script src="/static/vendor/htmx.min.js"></script>
<link rel="stylesheet" href="/static/app.css">
</head>
<body hx-boost="true">
{{ template "partials/nav.html" . }}
<main>
{{ block "content" . }}{{ end }}
</main>
</body>
</html>
{{ end }}
{{ define "pages/game.html" }}
{{ template "layouts/app.html" . }}
{{ end }}
{{ define "content" }}
<section>
<h1>{{ .Game.Name }}</h1>
<div id="scoreboard">
{{ template "partials/scoreboard.html" .Scoreboard }}
</div>
</section>
{{ end }}
A partial returns only the fragment being swapped:
{{ define "partials/scoreboard.html" }}
<section class="scoreboard">
<p>{{ .Home.Name }}: {{ .Home.Score }}</p>
<p>{{ .Away.Name }}: {{ .Away.Score }}</p>
</section>
{{ end }}
Use the page when the browser needs a document. Use the partial when HTMX needs a target to swap.
HTML Fragments Are the Primary UI API
In a GO(A)HT app, most UI changes should be handled by returning HTML fragments:
func (h *PartialHandlers) Scoreboard(w http.ResponseWriter, r *http.Request) {
vm := h.games.ScoreboardVM(r.Context(), r.PathValue("id"))
h.render.Partial(w, "partials/scoreboard.html", vm)
}
<div
id="scoreboard"
hx-ext="sse"
sse-connect="/games/{{ .Game.ID }}/events"
hx-get="/games/{{ .Game.ID }}/scoreboard"
hx-trigger="load, sse:refresh-scoreboard"
hx-swap="outerHTML"
>
{{ template "partials/scoreboard.html" .Scoreboard }}
</div>
The browser asks for HTML. The server answers with HTML. That keeps the UI contract inspectable in the network panel and easy to test with plain handler tests.
If you use hx-ext="sse", include the HTMX SSE extension script in your layout before relying on sse-connect.
Compose Pages From Fragments
A page template should mostly arrange fragments rather than duplicate markup:
{{ define "content" }}
{{ template "partials/game_header.html" .Header }}
{{ template "partials/scoreboard.html" .Scoreboard }}
{{ template "partials/player_list.html" .Players }}
{{ end }}
That gives you one source of truth for the UI surface that HTMX reuses later.
Rendering Helpers
Most apps benefit from two explicit render paths:
type Renderer struct {
tmpl *template.Template
}
func (r *Renderer) Page(w http.ResponseWriter, name string, data any) error {
return r.tmpl.ExecuteTemplate(w, name, data)
}
func (r *Renderer) Partial(w http.ResponseWriter, name string, data any) error {
return r.tmpl.ExecuteTemplate(w, name, data)
}
The implementation can be identical. The distinction still matters because it tells readers what surface the handler returns.
Template Data
Shape template data for the surface being rendered:
type GamePageVM struct {
Title string
Game GameSummary
Scoreboard ScoreboardVM
Players []PlayerVM
}
type ScoreboardVM struct {
Home TeamScoreVM
Away TeamScoreVM
}
Page view models can compose smaller fragment view models. Partial handlers can reuse the fragment-specific types directly.
Full-Page Refresh vs Partial Refresh
Both are valid. Choose based on the size of the UI change:
- refresh the full page when the whole document meaningfully changes
- refresh a partial when one section changes independently
BagTrax-style flows usually use:
- full-page HTML for initial loads and large navigation changes
- partial HTML for status panels, scoreboards, lists, and form feedback
Gotchas
- Parse all templates together so
defineandtemplatecalls resolve correctly. - Pass
.into nested templates unless you intentionally want an empty scope. - Keep fragment names stable. HTMX targets depend on them staying predictable.
- Do not introduce JSON just to update server-rendered markup. Return the fragment instead.