Skip to main content

Echo HTML Templates

Introduction

HTML templates are a crucial component of web applications, allowing you to generate dynamic HTML content based on data from your server. Echo, the high-performance Go web framework, provides excellent support for HTML templating through the standard Go html/template package as well as third-party templating engines.

In this guide, we'll explore how to implement HTML templates in your Echo applications, from basic setup to advanced usage patterns.

Understanding HTML Templates in Echo

At its core, HTML templating involves:

  1. Creating HTML files with special placeholders for dynamic content
  2. Registering these templates with your Echo application
  3. Rendering these templates with actual data when responding to HTTP requests

Echo doesn't include a built-in template renderer, but it provides a simple interface for integrating with Go's standard templating engines or third-party alternatives.

Setting Up HTML Templates in Echo

Basic Template Setup

First, let's create a simple directory structure for our templates:

myapp/
├── main.go
└── views/
├── layouts/
│ └── base.html
└── pages/
└── home.html

Now, let's create our base layout template (views/layouts/base.html):

html
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<header>
<h1>My Echo Application</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>

<main>
{{template "content" .}}
</main>

<footer>
<p>&copy; {{.year}} My Echo App</p>
</footer>
</body>
</html>

And our home page template (views/pages/home.html):

html
{{define "content"}}
<section>
<h2>Welcome, {{.name}}!</h2>
<p>This is a sample Echo application with HTML templates.</p>

{{if .items}}
<h3>Your Items:</h3>
<ul>
{{range .items}}
<li>{{.}}</li>
{{end}}
</ul>
{{else}}
<p>No items to display.</p>
{{end}}
</section>
{{end}}

Implementing the Template Renderer

Now, let's implement a basic template renderer for Echo:

go
package main

import (
"html/template"
"io"
"net/http"
"time"

"github.com/labstack/echo/v4"
)

// Define the template renderer
type TemplateRenderer struct {
templates *template.Template
}

// Implement the Render method for echo.Renderer interface
func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
e := echo.New()

// Create a new renderer with base template and all page templates
renderer := &TemplateRenderer{
templates: template.Must(template.ParseGlob("views/layouts/*.html")),
}

// Parse the page templates
template.Must(renderer.templates.ParseGlob("views/pages/*.html"))

// Set the renderer
e.Renderer = renderer

// Home route
e.GET("/", func(c echo.Context) error {
data := map[string]interface{}{
"title": "Home Page",
"name": "Echo User",
"year": time.Now().Year(),
"items": []string{"Item 1", "Item 2", "Item 3"},
}
return c.Render(http.StatusOK, "home.html", data)
})

// Start the server
e.Start(":8080")
}

When you run this application and navigate to http://localhost:8080, you'll see a rendered HTML page with:

  • A title "Home Page"
  • A welcome message for "Echo User"
  • A list of items
  • A footer with the current year

Template Caching and Hot Reloading

In production, you'll want to cache your templates for performance, but during development, hot reloading templates can be useful:

go
// Template renderer with conditional reloading
type TemplateRenderer struct {
templates *template.Template
templateDir string
reloadOnRender bool
}

func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
// Reload templates if enabled (for development)
if t.reloadOnRender {
t.templates = template.Must(template.ParseGlob(t.templateDir + "/layouts/*.html"))
template.Must(t.templates.ParseGlob(t.templateDir + "/pages/*.html"))
}
return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
// ...

// Create renderer with development mode
isDevelopment := true // Set based on environment
renderer := &TemplateRenderer{
templates: template.Must(template.ParseGlob("views/layouts/*.html")),
templateDir: "views",
reloadOnRender: isDevelopment,
}

template.Must(renderer.templates.ParseGlob("views/pages/*.html"))

e.Renderer = renderer

// ...
}

Advanced Template Features

Template Functions

You can add custom functions to your templates for more dynamic content:

go
// In your main.go
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("Jan 02, 2006")
},
"safeHTML": func(s string) template.HTML {
return template.HTML(s)
},
"add": func(a, b int) int {
return a + b
},
}

renderer := &TemplateRenderer{
templates: template.Must(template.New("").Funcs(funcMap).ParseGlob("views/layouts/*.html")),
}

Using these in your templates:

html
<p>Posted on: {{formatDate .publishDate}}</p>
<div>{{safeHTML .contentHTML}}</div>
<p>Total items: {{add (len .items) 1}}</p>

Nested Templates and Partials

For more modular code, you can create partial templates:

views/
├── layouts/
│ └── base.html
├── pages/
│ └── home.html
└── partials/
├── header.html
├── footer.html
└── sidebar.html

Define a partial (views/partials/header.html):

html
{{define "header"}}
<header>
<h1>{{.title}}</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
{{if .user}}
<span>Welcome, {{.user.Name}}</span>
{{else}}
<a href="/login">Login</a>
{{end}}
</nav>
</header>
{{end}}

And include it in your base layout:

html
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<!-- ... -->
</head>
<body>
{{template "header" .}}

<main>
{{template "content" .}}
</main>

{{template "footer" .}}
</body>
</html>

Don't forget to parse these partials in your application:

go
renderer := &TemplateRenderer{
templates: template.Must(template.ParseGlob("views/layouts/*.html")),
}
template.Must(renderer.templates.ParseGlob("views/pages/*.html"))
template.Must(renderer.templates.ParseGlob("views/partials/*.html"))

Practical Example: A Blog Application

Let's put everything together in a more complex example - a simple blog application:

go
package main

import (
"html/template"
"io"
"net/http"
"strconv"
"time"

"github.com/labstack/echo/v4"
)

// Blog post model
type Post struct {
ID int
Title string
Content string
ContentHTML template.HTML
CreatedAt time.Time
Author string
}

// Template renderer
type TemplateRenderer struct {
templates *template.Template
}

func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
e := echo.New()

// Set up static files
e.Static("/static", "public")

// Set up template functions
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("January 02, 2006")
},
"excerpt": func(s string, length int) string {
if len(s) <= length {
return s
}
return s[:length] + "..."
},
}

// Create the renderer
renderer := &TemplateRenderer{
templates: template.Must(template.New("").Funcs(funcMap).ParseGlob("views/layouts/*.html")),
}
template.Must(renderer.templates.ParseGlob("views/pages/*.html"))
template.Must(renderer.templates.ParseGlob("views/partials/*.html"))
e.Renderer = renderer

// Mock database of posts
posts := []Post{
{
ID: 1,
Title: "Getting Started with Echo",
Content: "Echo is a high performance, extensible, minimalist web framework for Go.",
ContentHTML: template.HTML("<p>Echo is a <strong>high performance</strong>, extensible, minimalist web framework for Go.</p>"),
CreatedAt: time.Now().AddDate(0, 0, -2),
Author: "John Doe",
},
{
ID: 2,
Title: "HTML Templates in Echo",
Content: "Learn how to use HTML templates effectively in your Echo applications.",
ContentHTML: template.HTML("<p>Learn how to use <em>HTML templates</em> effectively in your Echo applications.</p>"),
CreatedAt: time.Now().AddDate(0, 0, -1),
Author: "Jane Smith",
},
}

// Blog home route
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "blog_list.html", map[string]interface{}{
"title": "My Echo Blog",
"posts": posts,
"year": time.Now().Year(),
})
})

// Blog post route
e.GET("/post/:id", func(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.String(http.StatusBadRequest, "Invalid post ID")
}

// Find the post
var post Post
found := false
for _, p := range posts {
if p.ID == id {
post = p
found = true
break
}
}

if !found {
return c.String(http.StatusNotFound, "Post not found")
}

return c.Render(http.StatusOK, "blog_post.html", map[string]interface{}{
"title": post.Title,
"post": post,
"year": time.Now().Year(),
})
})

e.Start(":8080")
}

With corresponding templates:

views/pages/blog_list.html:

html
{{define "content"}}
<section class="blog-list">
<h1>Latest Blog Posts</h1>

{{range .posts}}
<article class="post-preview">
<h2><a href="/post/{{.ID}}">{{.Title}}</a></h2>
<div class="post-meta">
By {{.Author}} on {{formatDate .CreatedAt}}
</div>
<p>{{excerpt .Content 100}}</p>
<a href="/post/{{.ID}}" class="read-more">Read more</a>
</article>
{{else}}
<p>No blog posts available.</p>
{{end}}
</section>
{{end}}

views/pages/blog_post.html:

html
{{define "content"}}
<article class="blog-post">
<header>
<h1>{{.post.Title}}</h1>
<div class="post-meta">
Posted by {{.post.Author}} on {{formatDate .post.CreatedAt}}
</div>
</header>

<div class="post-content">
{{.post.ContentHTML}}
</div>

<footer>
<a href="/">&larr; Back to blog</a>
</footer>
</article>
{{end}}

Using Third-Party Template Engines

While the standard html/template package works well, you might prefer other templating engines like Pongo2 (Django-inspired) or Jet:

Using Pongo2

go
import (
"io"
"net/http"

"github.com/flosch/pongo2/v4"
"github.com/labstack/echo/v4"
)

type Pongo2Renderer struct {
debug bool
templateSet *pongo2.TemplateSet
}

func (r *Pongo2Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
var ctx pongo2.Context

if data != nil {
ctx = data.(pongo2.Context)
}

tmpl, err := r.templateSet.FromFile(name)
if err != nil {
return err
}

return tmpl.ExecuteWriter(ctx, w)
}

func main() {
e := echo.New()

renderer := &Pongo2Renderer{
debug: true,
templateSet: pongo2.NewSet("templates", pongo2.MustNewLocalFileSystemLoader("views")),
}

e.Renderer = renderer

e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "pages/home.html", pongo2.Context{
"title": "Home Page",
"name": "Echo User",
"items": []string{"Item 1", "Item 2", "Item 3"},
})
})

e.Start(":8080")
}

Summary

HTML templates are a fundamental part of web application development with Echo. In this guide, we've covered:

  • Setting up a basic template renderer with Echo
  • Creating layouts and content templates
  • Adding template functions for more dynamic content
  • Implementing template caching and development hot-reloading
  • Working with nested templates and partials
  • Building a complete practical example with a blog application
  • Using third-party template engines

By mastering HTML templates in Echo, you can create rich, dynamic web applications while maintaining clean separation between your business logic and presentation layer.

Additional Resources

Exercises

  1. Simple Portfolio Site: Create a simple portfolio website with an index page, about page, and projects page using Echo templates.

  2. Template Inheritance: Implement a template inheritance system with multiple layout options (e.g., one-column, two-column) that content pages can choose from.

  3. Dynamic Navigation: Create a navigation system that highlights the current page and conditionally shows menu items based on user authentication status.

  4. Form Handling: Build a contact form template that preserves user input when validation fails and displays appropriate error messages.

  5. Pagination Component: Create a reusable pagination component template that can be included in any list view with configurable page size and current page parameters.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)