BuildBot

HTML Over the Wire

Templates that scale

Lesson 2 of 5

What you'll learn

  • Parse templates once at startup with //go:embed and template.Must
  • Define named partials that serve both full-page and fragment responses
  • Rely on html/template auto-escaping — and never build HTML with fmt.Sprintf

If every handler answers with HTML, templates are your app's entire view layer. Go's html/template is in the standard library, and the setup is three lines: embed the files into the binary (the same //go:embed from the previous course), parse them all at startup, and let template.Must panic at boot if any template is broken — a template typo should kill the process at start, not a request at 2am.

import (
    "embed"
    "html/template"
)

//go:embed templates
var templateFS embed.FS

var tmpl = template.Must(template.ParseFS(templateFS, "templates/*.html"))

The pattern that scales is one file, many named templates — a page plus the partials it's built from:

<!-- templates/todos.html -->
{{define "page"}}
<!doctype html>
<script src="/static/htmx.min.js"></script>
<ul id="list">
  {{range .Todos}}{{template "todo-item" .}}{{end}}
</ul>
{{end}}

{{define "todo-item"}}
<li id="todo-{{.ID}}">{{.Title}}</li>
{{end}}

Now the two response shapes from lesson 1 come from the same partial:

mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
    tmpl.ExecuteTemplate(w, "page", data)      // initial GET: the full page
})
mux.HandleFunc("POST /todos", func(w http.ResponseWriter, r *http.Request) {
    todo := store.Add(r.FormValue("title"))
    tmpl.ExecuteTemplate(w, "todo-item", todo) // htmx: just the fragment
})

The row markup lives in exactly one place. Change todo-item and the full page render and every fragment response change together — they cannot drift.

The other reason to insist on html/template (not text/template, not string building): context-aware auto-escaping. {{.Title}} is escaped for wherever it appears — HTML-escaped in element bodies, attribute-escaped inside attributes, URL-escaped inside href. A title of <script>alert(1)</script> renders as inert text. In a hypermedia app every response is HTML built from user data, so this isn't a nicety — it's your XSS defense on every request.

Never fmt.Sprintf your HTML

fmt.Fprintf(w, "<li>%s</li>", title) compiles, works in the demo, and is an XSS hole: user input goes into the page unescaped. If HTML leaves your server, it goes through html/template. No exceptions for "small" fragments — those are exactly the ones built from user input.

The challenge models the two habits: one partial rendered both inside a page and alone as a fragment, with escaping applied on the way in.

One partial, two responses (JS model)

Run it. The todoItem partial renders inside the full page AND alone as the htmx fragment, and the esc() step (modeling auto-escaping) turns the hostile title into inert text. Remove esc() from todoItem to see the XSS slip through.

Loading editor…
Knowledge check

Why render the same todo-item partial from both the full-page handler and the fragment handler?

Next: the write path — form POSTs, server-side validation, and re-rendering the form fragment with inline errors and a 422.

Saved on this device. Sign in to sync your progress everywhere.