skip to Main Content

I have been trying to understand the html/template package in combination with the embed package in golang 1.23 but I am struggling to even get a simple base layout with two pages working. As detailed below I encounter some overwriting of content (at least I suspect it to be the issue) where one and the same html render is displayed for different routes.

The file structure of my minimal example is as follows:

- templates/
    - layout.html
    - page1.html
    - page2.html

- main.go
- go.mod

The server embeds the html files and serves them with the standard net/http package (1.23):

main.go

package main

import (
    "embed"
    "html/template"
    "log"
    "net/http"
)

//go:embed templates/*.html
var templateFS embed.FS

var templates *template.Template

func main() {
    var err error

    templates, err = template.ParseFS(templateFS, "templates/*.html")
    if err != nil {
        log.Fatal("Error parsing templates: ", err)
    }

    http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, "/page1", http.StatusSeeOther)
    })

    http.HandleFunc("GET /page1", func(w http.ResponseWriter, r *http.Request) {
        err := templates.ExecuteTemplate(w, "page1", nil)
        if err != nil {
            http.Error(w, "Template rendering error: "+err.Error(), http.StatusInternalServerError)
        }
    })

    http.HandleFunc("GET /page2", func(w http.ResponseWriter, r *http.Request) {
        err := templates.ExecuteTemplate(w, "page2", nil)
        if err != nil {
            http.Error(w, "Template rendering error: "+err.Error(), http.StatusInternalServerError)
        }
    })

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Now the way I attempted to design the templates is the typical base layout which gets "filled" through sub-pages.

layout.html

{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{{ block "title" . }}My Website{{ end }}</title>
</head>
<body>
    <header>
        <h1>Welcome!</h1>
        <nav>
            <a href="/page1">Page 1</a> | 
            <a href="/page2">Page 2</a>
        </nav>
    </header>
    <main>
        {{ block "content" . }}{{ end }}
    </main>
</body>
</html>
{{ end }}

And the two simple sub pages:

page1.html

{{ define "page1"}}
{{ template "layout" . }}
{{ end }}

{{ block "title" . }}Page 1{{ end }}

{{ block "content" . }}
    <h2>This is Page 1</h2>
    <p>Content for page 1 goes here.</p>
{{ end }}

page2.html

{{ define "page2"}}
{{ template "layout" . }}
{{ end }}

{{ block "title" . }}Page 2{{ end }}

{{ block "content" . }}
    <h2>This is Page 2</h2>
    <p>Content for page 2 goes here.</p>
{{ end }}

Now what happens is that for http://localhost:8080/page1 and http://localhost:8080/page2 the template for page 2 is displayed, even if I switch the name string in the ExecuteTemplate function call for both routes.

I have been experimenting quite a lot with the templates of page 1/2 (shifting the block declarations in and out of the define, using the template action instead of the block action, etc.). I suspect I am missing some very crucial fundamentals when it comes to golang’s template package or some side effects introduced by the embed package. Can you guys give me a hint?

(I would love to do this with the standard package, not temlp, etc.)

2

Answers


  1. Chosen as BEST ANSWER

    As pointed out by Burak Serdar, explicit parsing in the correct order fixed the problem for me. I am not sure if this is the idiomatic way to do it since I am fairly new to go.

    After some further reading in the docs, I stumbled upon the section "Nested template definitions" in text/html docs (text/template and html/template have identical interfaces) which seems to confirm this approach:

    If it's necessary to have a template addressable from multiple associations, the template definition must be parsed multiple times to create distinct *Template values, or must be copied with Template.Clone or Template.AddParseTree.

    To anyone interested, here is the modified code which seems to be working as intended:

    main.go

    package main
    
    import (
        "embed"
        "html/template"
        "log"
        "net/http"
    )
    
    //go:embed templates/*.html
    var templateFS embed.FS
    
    var (
        templatePage1 *template.Template
        templatePage2 *template.Template
    )
    
    func main() {
        var err error
    
        templatePage1, err = template.ParseFS(templateFS, "templates/layout.html", "templates/page1.html")
        if err != nil {
            log.Fatal("Error parsing template for page 1: ", err)
        }
    
        templatePage2, err = template.ParseFS(templateFS, "templates/layout.html", "templates/page2.html")
        if err != nil {
            log.Fatal("Error parsing template for page 2: ", err)
        }
    
    
        http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
            http.Redirect(w, r, "/page1", http.StatusSeeOther)
        })
    
        http.HandleFunc("GET /page1", func(w http.ResponseWriter, r *http.Request) {
            err := templatePage1.ExecuteTemplate(w, "layout", nil)
            if err != nil {
                http.Error(w, "Template rendering error for page 1: "+err.Error(), http.    StatusInternalServerError)
            }
        })
    
        http.HandleFunc("GET /page2", func(w http.ResponseWriter, r *http.Request) {
            err := templatePage2.ExecuteTemplate(w, "layout", nil)
            if err != nil {
                http.Error(w, "Template rendering error for page 2: "+err.Error(), http.    StatusInternalServerError)
            }
        })
    
        log.Println("Server started on :8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    

    layout.html

    Unchanged
    

    page1.html

    {{ block "title" . }}Page 1{{ end }}
    
    {{ block "content" . }}
        <h2>This is Page 1</h2>
        <p>Content for page 1 goes here.</p>
    {{ end }}
    

    page2.html

    {{ block "title" . }}Page 2{{ end }}
    
    {{ block "content" . }}
        <h2>This is Page 2</h2>
        <p>Content for page 2 goes here.</p>
    {{ end }}
    

    Thank you guys.


  2. Ordering of the templates is important when you parse them. Try this:

    1. Define the layout with references to other templates:
    <!DOCTYPE HTML>
    ...
    {{template "title" .}}
    ...
    {{template "content" .}}}
    ...
    
    {{define "title"}}{{end}}
    {{define "content"}}{{end}}
    
    1. Define individual pages
    page1: 
    {{define "title"}}Title1 contents{{end}}
    {{define "content"}}Page 1 Contents{{end}}
    
    page2:
    {{define "title"}}Title2 contents{{end}}
    {{define "content"}}Page 2 Contents{{end}}
    
    1. Parse two templates, one for page1 and one for page2. Page1 should include layout and page1. Page2 should include layout and page2.
    page1Templ := template.Must(template.New("p1").Parse(layout))
    template.Must(page1Templ.Parse(page1))
    page2Templ := template.Must(template.New("p2").Parse(layout))
    template.Must(page2Templ.Parse(page2))
    
    1. Execute page1Templ for page 1, page2Templ for page 2.

    This way each template will include the layout and the corresponding page definitions.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search