Template nesting in (text|html)/template

· 352 words · 2 minute read

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.