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
safeHTMLunless 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.