HTML Over the Wire
Templates that scale
Lesson 2 of 5
What you'll learn
- Parse templates once at startup with
//go:embedandtemplate.Must - Define named partials that serve both full-page and fragment responses
- Rely on
html/templateauto-escaping — and never build HTML withfmt.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.
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.
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.