I recently tried to build a web application using Go and HTML templates. In doing so, I started to miss some cool features that Buffalo and plush have to avoid repeating the layout of your web pages. I thought this was a common enough problem that the Go team should have provided the tools in the standard library, and as usual, they did. That’s when I came across the block and define expressions for templates. In this post I will explain how to use these to avoid repeating the layout of your web pages.

The application

To better explain the issue at hand let’s take a blog as an example. On a blog we have a layout for the header and footer of each page, and a template for each post. The content of each post is different but the content of the header and footer is the same.

A ten thousand feet view of the application looks like the following file tree:

blog
  cmd
    blog
      main.go
  server
    server.go
  templates
    posts
      antonio-likes-go.html
      wawandco-rocks.html
      teams-that-matter.html

Now, the problem comes from having repeated HTML for the header and footer of the page across every blog post template. The following content repeats in all templates:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/stylesheet/path" />
    <title>{{.Year}}</title>
  </head>
  <body>
    <h1>Welcome to the blog!</h1>
    ...[page content]...
    <footer>
      <p>&copy; {{.Title}} by Blog Team</p>
    </footer>
  </body>
</html>

And when we think about the SRP, the reasons why this code would change are different than the reasons why the content of the templates will. It makes sense to try and refactor to avoid repetition and, more importantly, to avoid maintaining the same code in all templates.

Adding a layout file

This is where the block and define expressions come in handy. To use them, we will add a layout file to the templates folder. The layout file will hold the HTML we’ve mentioned it repeats across the posts.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/stylesheet/path.css" />
    <title>{{.Title}}</title>
  </head>
  <body>
    <h1>Welcome to the blog!</h1>
    <div class="content">
      <!-- Here goes the content of the template -->
      {{ block "content" . }}{{ end }}
    </div>
    <footer>
      <p>&copy; {{.Year}} by Blog Team</p>
    </footer>
  </body>
</html>

With the slight difference that we’ve added the block expression to specify the block where the content of the posts will take place. Besides that we need to modify our posts, since we have extracted the layout we can remove the repeated content from the posts.

As an example let’s take templates/posts/wawandco-rocks.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/stylesheet/path.css" />
    <title>{{.Title}}</title>
  </head>
  <body>
    <h1>Welcome to the blog!</h1>
    <div class="content">
      <h2>Wawandco Team Rocks</h2>
      <p>
        The Wawandco team is amazing. I don't have to tell you this, these guys
        know very well Go and put the best effort to build the best software
        solutions for Wawandco clients.
      </p>
    </div>
    <footer>
      <p>&copy; {{.Year }} by Blog Team</p>
    </footer>
  </body>
</html>

Which will be simplified to the following:

<h2>Wawandco Team Rocks</h2>
<p>
  The Wawandco team is amazing. I don't have to tell you this, these guys know
  very well Go and put the best effort to build the best software solutions for
  Wawandco clients.
</p>

We can start seeing the difference in content for each of the posts that gets simplified. While modifying the content of the posts and adding the layout is important now we need to modify the way we render these posts.

Modifying the post server

As mentioned, we need to modify the way we render those posts to inject the post content into the layout. To do that we will modify the server/server.go file, which contains the following code:

package server

// Data needed by all the pages.
var commonData = struct {
    Year  string
    Title string
}{
    Year:  time.Now().Format("2006"),
    Title: "The Blog",
}

func renderPost(w http.ResponseWriter, r *http.Request) {
    // Read the template requested from the posts directory.
    dd, err = ioutil.ReadFile(filepath.Join("./templates/posts/", r.URL.Path))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Executing the template
    if err := tmpl.Execute(w, commonData); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

// New returns a new instance of the blog server which renders
// the blog posts using the blog templates.
func New() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/", renderPost)

    return mux
}

We should now read the layout first and then read the content of the post to inject it into the layout. The resulting server.go file is as follows:

package server

// Data needed by all the pages.
var commonData = struct {
    Year  string
    Title string
}{
    Year:  time.Now().Format("2006"),
    Title: "The Blog",
}

func renderPost(w http.ResponseWriter, r *http.Request) {
    // Here we read the layout to later on instantiate a
    // template with it.
    dd, err := ioutil.ReadFile("./templates/layout.html")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // We parse the layout content as a template.
    tmpl, err := template.New("layout").Parse(string(dd))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Then we read the content of the post.
    dd, err = ioutil.ReadFile(filepath.Join("./templates/posts/", r.URL.Path))
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Here is the magic ✨. We use {{define "content"}} to inject the content of
    // the post into the layout. It will be inserted in the place where
    // the layout has defined the block named "content".
    tt := fmt.Sprintf(`{{define "content"}}%s{{end}}`, dd)
    tmpl, err = template.Must(tmpl.Clone()).Parse(tt)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    data := struct {
        Year  string
        Title string
    }{
        Year:  time.Now().Format("2006"),
        Title: "The Blog",
    }

    if err := tmpl.Execute(w, data); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

// New returns a new instance of the blog server which renders
// the blog posts using the blog templates.
func New() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/", renderPost)

    return mux
}

The magic in this file happens in reading and cloning the template, we first read the layout and then we read the content of the post and inject it into the layout.


    // We parse the layout content as a template.
    tmpl, err := template.New("layout").Parse(string(dd))
    ...

    // Here is the magic ✨. We use {{define "content"}} to inject the content of
    // the post into the layout. It will be inserted in the place where
    // the layout has defined the block named "content".
    tt := fmt.Sprintf(`{{define "content"}}%s{{end}}`, dd)
    tmpl, err = template.Must(tmpl.Clone()).Parse(tt)

To do it we use the {{define "content"}} block and then we inject the content of the post within that expression.

Improvements and caveats

In order to illustrate this concept of layouts for Go templates I simplified the blog post server, in doing so I traded some software attributes for simplicity. I wanted to point some important points:

  • The blog pose server is not secure as it reads from the filesystem.
  • We could have implemented some caching layer for the layout.
  • The templates could be embedded instead of read from the filesystem.

While these are cool things we could do, I thought I wanted to concentrate in showing how to do the template layout part. Maybe we could cover these as part of other of my posts. You can check the source code of the blog server in the following git repository.

Wrapping up

That’s all I got. I hope you enjoyed this post. If you find it useful or have any comment I’d love to hear from you, reach me out at @paganotoni in Twitter or our team at @Wawandco.