Today Problem
Welcome back, folks! Today's problem ❌ is how to embed one HTML template into another. In this way, we can foster the reusability of the code and make it less redundant. Let's pretend to have two HTML pages to render. These are identical unless for the middle part of the page. Why don't we have a header, a footer, and two bodies with only the specific page's information? You already figured it out since it's a typical use case. It's worth to cover. Let's see how we can achieve it with Go and the Gin web framework.
What is HTML Templating?
Before getting straight into the code, let's talk a little about HTML templating. It allows you to blend HTML tags with values provided later. Gin manages the HTML templating by empowering the usage of the package html/template
that belongs to Go's Standard Library. You can read more here. Now, let's start presenting the solution 🚀.
The solution
First, let's look at the HTML templates, and then we'll see how to invoke them in the code and how to run some tests. Let's present the final HTML pages we'd like to have:
As you can see, the headers and footers are identical. However, the "core" parts differ based on the requested page.
HTML Templates
All the templates lay in the templates
folder. The files are:
header.tmpl
page1.tmpl
page2.tmpl
footer.tmpl
First, let's cover the static ones.
The header.tmpl
file
Its content is:
{{define "header" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .title }}</title>
</head>
<body>
<h1>BASELINE</h1>
<div id="content">
{{end}}
Here, there are a couple of things to pay attention to. All the template's code resides between the start and end definitions of the template. In this case, the boundaries are {{define "header" }}
and {{end}}
. Then, we use a parameter called title
within the <title>
tag that can be assigned whenever we invoke this template from the child templates. More on that later.
The footer.tmpl
file
Here, the content is even easier:
{{define "footer"}}
</div>
<p>FooterInfo</p>
</body>
</html>
{{end}}
The footer
template is the final part of all the pages that will always be consistent.
The page1.tmpl
file
Now, the spicy part 🌶️. The HTML part that changes lives in the page1.tmpl
file. The content is as follows:
{{template "header" . }}
<h2>Welcome to Page 1</h2>
<p>This is the content of Page 1.</p>
{{template "footer"}}
Here, there are a couple of things worth mentioning. First, we add the reference to the header
template by using the line {{template "header" . }}
.
There's a 1:1
relationship between the names used in the header.tmpl
and page1.tmpl
files (e.g. in both we used header
).
The .
means to pass along over the pipeline all the parameters. Thanks to this way, we can pass the value Page 1 IS HERE
to the page1.tmpl
file that, in turn, will pass it to the header
template that is the one using it.
Then, we type the specific content for this page and reference the footer
template to spread consistency around 🎇.
Please be aware that I skipped the code for the
page2.tmpl
file since it's almost identical to the one just seen.
Now, let's take a look at the Go code.
The Go Code
Here, we have to tie everything up and be able to respond to the HTTP endpoints. For the sake of the demo, the whole code is in the main.go
file. First, let me present the code, and then I'll walk you through all the relevant sections of it.
package main
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
gin.SetMode(gin.DebugMode)
r := gin.Default()
// load templates
r.LoadHTMLGlob("templates/*.tmpl")
r.GET("/page1", func(c *gin.Context) {
c.HTML(http.StatusOK, "page1.tmpl", gin.H{
// this value goes to the "header.tmpl" file
"title": "Page 1 IS HERE",
})
})
r.GET("/page2", func(c *gin.Context) {
c.HTML(http.StatusOK, "page2.tmpl", gin.H{
"title": "Page 2 IS HERE",
})
})
if err := r.Run(":8080"); err != nil {
fmt.Fprintf(os.Stderr, "cannot run the srv: %v\n", err)
os.Exit(1)
}
}
The first noticeable thing is the r.LoadHTMLGlob("templates/*.tmpl")
call. It loads all the matching templates in a specific directory. For completeness, you could also have used r.LoadHTMLFiles()
to specify the files you want to load.
In real-life projects, do not hard-code the templates' filesystem path in your code.
Then, we instrument the router to answer GET
HTTP requests that hit the /page1
and /page2
addresses. Within the HandlerFunc
body, we used the method c.HTML
that allows us to set:
- The HTTP Response Status Code
- The template to use for this kind of request
- Optional parameters we may want to pass to the template to make it more dynamic
We used the type gin.H
for parameters to have great flexibility since it's based on the underlying type map[string]any
.
Finally, we launch our server and catch 🔎 the eventual error.
Give It a Try
Now, let's make sure that everything works as expected. Issue go run .
, and checks that the server started without problems. To test it out, we're going to use the curl
command (you may need to install it on your machine. More on it here):
curl http://localhost:8080/page1
The output printed in the shell should be this one:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page 1 IS HERE</title>
</head>
<body>
<h1>BASELINE</h1>
<div id="content">
<h2>Welcome to Page 1</h2>
<p>This is the content of Page 1.</p>
</div>
<p>FooterInfo</p>
</body>
</html>
You can also try it directly from the web browser of your choice. It doesn't matter, you'll have the same result.
That's a Wrap
This blog post is not exhaustive about HTML templating. I encourage you to have a look at these resources:
- https://gin-gonic.com/docs/examples/html-rendering/
- https://www.digitalocean.com/community/tutorials/how-to-use-templates-in-go
- https://pkg.go.dev/html/template
Any feedback is highly appreciated.
Before leaving, I strongly invite you to reach out if you are interested in some topics and want me to address them. I swear I shortlist them and do my best to deliver helpful content.
Thank you very much for the support, and see you in the next one 👋
Top comments (0)