HTML Templates

Go templates are made to be very adaptable. This allows for many different approaches for template architectures. This guide builds a simple and extensible parent/child template system. If you are coming from a React ecosystem, you might like Templ.

Here is what the most basic Go template looks like:

package main

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

func main() {
    // Define a template
    tmpl := template.Must(template.New("hello").Parse(`
        <!DOCTYPE html>
        <html>
        <body>
            <h1>Hello, {{.}}!</h1>
        </body>
        </html>
    `))

    // Make a router
    mux := http.NewServeMux()

    // Define a handler to run if a request to the webserver
    mux.HandleFunc(" GET /hello", getHello)

    http.ListenAndServe(":8000", mux)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    tmpl.Execute(w, "World") // You can change "World" to any string
}

The go template documentation lists all of the different things you can do in templates.

A production ready example

Here is a production ready example from jaxlo.net. Notice that the errors tell the end user that something went wrong. But the exact error ,with potential sensitive data, is sent to the terminal/log.

func ServeTemplates(w http.ResponseWriter, data any, templatePaths []string) error {
	// If there is a base template, it should be first in templatePaths
	t, err := template.ParseFiles(templatePaths...)
	if err != nil {
		http.Error(w, "Error parsing template", http.StatusInternalServerError) // This tells the user something went wrong
		log.Println("Error parsing template:", err)
		return err
	}
	err = t.Execute(w, data)
	if err != nil {
		http.Error(w, "Error executing template", http.StatusInternalServerError)
		log.Println("Error executing template:", err)
		return err
	}
	return nil
}

This is what this looks like when used in a handler with multiple variables.

func GetPosts(w http.ResponseWriter, r *http.Request) {
	posts := Posts() // Defined elsewhere. This can also be a database lookup
	ServeTemplates(w, map[string]any{"r": r, "posts": posts}, []string{"views/base.html", "views/home.html"})
}

This is what the HTML template looks like for posts:

<h2>Posts</h2>
{{range .posts}}
    <div>
        <h3>
            <a href="/project/{{.Codename}}">{{.Title}}</a>
        </h3>
        <p>{{.Date}} |</span> {{.Subtitle}}</p>
    </div>
{{end}}

And this is what it looks like to apply a particular css class when the URL path matches one in the nav bar:

<nav>
    <a href="/" {{if eq .r.URL.Path "/"}}class="nav-active"{{end}}>Home</a>
    <a href="/books" {{if eq .r.URL.Path "/books"}}class="nav-active"{{end}}>Book Reviews</a>
    <a href="/sites" {{if eq .r.URL.Path "/sites"}}class="nav-active"{{end}}>Sites</a>
    <a href="/guestbook" {{if eq .r.URL.Path "/guestbook"}}class="nav-active"{{end}}>Guestbook</a>
</nav>