Go includes a safe and somewhat performant templating library. But the best way to structure code using html/template has eluded me for a long time.
I like to write a single base layout used on separate pages in order to keep the
pages looking consistent. I read about {{block}}
in
text/template which was described as “A
block is shorthand for defining a template {{define "name"}} T1 {{end}}
and then
executing it in place {{template "name" pipeline}}
”
So I tried:
<!-- base.html -->
{{define "base"}}
<html>
<head>
<title>{{block "title" .}}{{.}}{{end}}</title>
<link rel="stylesheet" href="style.css">
<!-- and other shared stuff -->
</head>
<body>
{{block "body" .}}
<h1>override this</h1>
{{end}}
</body>
</html>
{{end}}
And then:
<!-- page1.html -->
{{define "title"}}Page 1{{end}}
{{define "body"}}
<h1>Page 1</h1>
<p>Content</p>
{{end}}
{{template "base" $}}
<!-- page2.html -->
{{define "title"}}Page 2{{end}}
{{define "body"}}
<h1>Page 2</h1>
<p>Content</p>
{{end}}
{{template "base" $}}
This does not work if you parse them all with template.ParseFS(). Templates in
Go aren’t really nested in the *template.Template
structure. It’s more like a
map[name]*Template
, which means that there’s a flat namespace. (It’s actually
not possible to define a template inside a template even if the documentation
for block
makes that sound like an option.) You can make it work if you parse
and combine templates carefully with:
base := template.Must(template.ParseFiles("base.html"))
page1raw, _ := os.ReadFile("page1.html")
page1 := template.Must(base.Clone()).Parse(page1raw)
page2raw, _ := os.ReadFile("page2.html")
page2 := template.Must(base.Clone()).Parse(page2raw)
That felt too cumbersome to me. So in the end I went with a layout like this:
<!-- base.html -->
{{define "header"}}
<html>
<head>
<title>{{$.Title}}</title>
<link rel="stylesheet" href="style.css">
<!-- and the rest of the shared stuff -->
</head>
<body>
{{end}}
{{define "footer"}}
</body>
</html>
{{end}}
<!-- page1.html -->
{{template "header" $}}
<h1>Page 1</h1>
<p>Content</p>
{{template "footer" $}}
<!-- page2.html -->
{{template "header" $}}
<h1>Page 2</h1>
<p>Content</p>
{{template "footer" $}}
This can then be used with:
// ParseFS for fs.FS or embed.FS
templates := template.Must(template.ParseFS(os.DirFS("."), "*.html"))
templates.ExecuteTemplate(w, "page1.html", map[string]any{"Title":"Page 1"})
The downside is that you need to pass in the title and other base layout parameters via template parameters. But as I need to pass in data to the templates anyway it’s not a huge inconvenience.