Skip to main content

Echo Multi-template Engines

Introduction

In web development, template engines are powerful tools that allow you to generate dynamic HTML content by combining static templates with changing data. Echo, a high-performance web framework for Go, provides flexible support for multiple template engines through its renderer interface.

This guide will explore how to configure and use multiple template engines within a single Echo application. This approach gives you the flexibility to choose the most appropriate template engine for different parts of your application or to gradually migrate from one template engine to another.

Understanding Multi-template Engines in Echo

By default, Echo doesn't include a template engine, giving developers the freedom to choose the one that best fits their needs. Echo supports any template engine that implements the Renderer interface:

go
type Renderer interface {
Render(w io.Writer, name string, data interface{}, c Context) error
}

This simple but powerful interface enables Echo to work with multiple template engines concurrently, allowing you to leverage the strengths of different engines in the same application.

Setting Up Multiple Template Engines

Step 1: Create a Custom Multi-Renderer

First, we'll create a custom structure that will hold multiple renderer implementations:

go
package main

import (
"github.com/labstack/echo/v4"
"html/template"
"io"
)

// MultiRenderer holds multiple renderer implementations
type MultiRenderer struct {
renderers map[string]echo.Renderer
defaultRenderer string
}

// NewMultiRenderer creates a new MultiRenderer instance
func NewMultiRenderer() *MultiRenderer {
return &MultiRenderer{
renderers: make(map[string]echo.Renderer),
}
}

// Add registers a renderer with a name
func (m *MultiRenderer) Add(name string, renderer echo.Renderer) {
m.renderers[name] = renderer
if m.defaultRenderer == "" {
m.defaultRenderer = name
}
}

// SetDefault sets the default renderer by name
func (m *MultiRenderer) SetDefault(name string) {
if _, exists := m.renderers[name]; exists {
m.defaultRenderer = name
}
}

// Render implements the echo.Renderer interface
func (m *MultiRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
// Split name to extract renderer name and template name
parts := strings.SplitN(name, ":", 2)

rendererName := m.defaultRenderer
templateName := name

// If the name contains a renderer specifier, use it
if len(parts) == 2 {
rendererName = parts[0]
templateName = parts[1]
}

// Get the requested renderer
renderer, exists := m.renderers[rendererName]
if !exists {
return fmt.Errorf("renderer '%s' not found", rendererName)
}

// Delegate rendering to the specific renderer
return renderer.Render(w, templateName, data, c)
}

Step 2: Implement Standard Template Engine Renderers

Now, let's create implementations for some common template engines:

HTML/Template (Go's standard library)

go
// GoTemplateRenderer renders templates using Go's html/template package
type GoTemplateRenderer struct {
templates *template.Template
}

// NewGoTemplateRenderer creates a new GoTemplateRenderer
func NewGoTemplateRenderer(templatesDir string) (*GoTemplateRenderer, error) {
templates, err := template.ParseGlob(templatesDir + "/*.html")
if err != nil {
return nil, err
}

return &GoTemplateRenderer{
templates: templates,
}, nil
}

// Render implements the echo.Renderer interface
func (r *GoTemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return r.templates.ExecuteTemplate(w, name, data)
}

Step 3: Integrate with Other Template Engines

Let's implement a renderer for another popular template engine like Pongo2 (a Django-inspired template engine for Go):

go
import (
"github.com/flosch/pongo2/v4"
)

// Pongo2Renderer renders templates using the Pongo2 template engine
type Pongo2Renderer struct {
templateSet *pongo2.TemplateSet
templateDir string
}

// NewPongo2Renderer creates a new Pongo2Renderer
func NewPongo2Renderer(templateDir string) *Pongo2Renderer {
loader := pongo2.MustNewLocalFileSystemLoader(templateDir)
set := pongo2.NewSet("default", loader)

return &Pongo2Renderer{
templateSet: set,
templateDir: templateDir,
}
}

// Render implements the echo.Renderer interface
func (r *Pongo2Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
template, err := r.templateSet.FromFile(name)
if err != nil {
return err
}

// Convert data to pongo2.Context
var pongoContext pongo2.Context
if data != nil {
if contextData, ok := data.(map[string]interface{}); ok {
pongoContext = pongo2.Context(contextData)
} else {
pongoContext = pongo2.Context{"data": data}
}
}

return template.ExecuteWriter(pongoContext, w)
}

Step 4: Register and Use Multiple Engines

Now we can register and use these different renderers in our Echo application:

go
package main

import (
"github.com/labstack/echo/v4"
"net/http"
)

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

// Create multi-renderer
renderer := NewMultiRenderer()

// Add Go's html/template renderer
goTemplateRenderer, err := NewGoTemplateRenderer("./views/go-templates")
if err != nil {
e.Logger.Fatal(err)
}
renderer.Add("go", goTemplateRenderer)

// Add Pongo2 renderer
pongo2Renderer := NewPongo2Renderer("./views/pongo2")
renderer.Add("pongo", pongo2Renderer)

// Set default renderer
renderer.SetDefault("go")

// Set the renderer for Echo
e.Renderer = renderer

// Define routes
e.GET("/", func(c echo.Context) error {
// Use default renderer (Go templates)
return c.Render(http.StatusOK, "index.html", map[string]interface{}{
"name": "World",
})
})

e.GET("/pongo", func(c echo.Context) error {
// Explicitly specify the Pongo2 renderer
return c.Render(http.StatusOK, "pongo:index.html", map[string]interface{}{
"name": "World from Pongo2",
})
})

e.GET("/go", func(c echo.Context) error {
// Explicitly specify the Go templates renderer
return c.Render(http.StatusOK, "go:index.html", map[string]interface{}{
"name": "World from Go Templates",
})
})

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

Example Template Files

Let's create example templates for both engines:

./views/go-templates/index.html:

html
<!DOCTYPE html>
<html>
<head>
<title>Go Template Example</title>
</head>
<body>
<h1>Hello, {{.name}}!</h1>
<p>This page was rendered using Go's standard html/template package.</p>
</body>
</html>

./views/pongo2/index.html:

html
<!DOCTYPE html>
<html>
<head>
<title>Pongo2 Template Example</title>
</head>
<body>
<h1>Hello, {{ name }}!</h1>
<p>This page was rendered using the Pongo2 template engine.</p>
</body>
</html>

Practical Use Cases

1. Migrating Between Template Engines

Multi-template engines are particularly useful when migrating from one template engine to another. You can convert templates one by one while keeping your application running:

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

renderer := NewMultiRenderer()
renderer.Add("old", oldTemplateRenderer)
renderer.Add("new", newTemplateRenderer)

// Start with the old renderer as default
renderer.SetDefault("old")

e.Renderer = renderer

// Routes using new templates
e.GET("/new-feature", func(c echo.Context) error {
return c.Render(http.StatusOK, "new:feature.html", data)
})

// Legacy routes using old templates
e.GET("/legacy-feature", func(c echo.Context) error {
return c.Render(http.StatusOK, "old:feature.html", data)
})
}

2. Using Different Engines for Different Content Types

You might choose different template engines based on their strengths:

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

renderer := NewMultiRenderer()

// Use Go templates for HTML pages
renderer.Add("html", htmlTemplateRenderer)

// Use a specialized JSON template engine for API responses
renderer.Add("json", jsonTemplateRenderer)

// Use a specialized email template engine
renderer.Add("email", emailTemplateRenderer)

e.Renderer = renderer

e.GET("/profile", func(c echo.Context) error {
return c.Render(http.StatusOK, "html:profile.html", userData)
})

e.GET("/api/profile", func(c echo.Context) error {
return c.Render(http.StatusOK, "json:profile.json", userData)
})

e.POST("/send-welcome", func(c echo.Context) error {
// Render email template and send it
var buf bytes.Buffer
if err := e.Renderer.Render(&buf, "email:welcome.html", userData, c); err != nil {
return err
}

// Send the email with the rendered template
sendEmail(userData.Email, buf.String())
return c.NoContent(http.StatusOK)
})
}

3. A/B Testing Different Template Implementations

You can even implement A/B testing using different template renderers:

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

renderer := NewMultiRenderer()
renderer.Add("versionA", versionARenderer)
renderer.Add("versionB", versionBRenderer)

e.Renderer = renderer

e.GET("/product/:id", func(c echo.Context) error {
// Simple A/B test based on user ID or random selection
userID := getUserID(c)
version := "versionA"
if userID % 2 == 0 {
version = "versionB"
}

return c.Render(http.StatusOK, version + ":product.html", productData)
})
}

Performance Considerations

When using multiple template engines, keep these performance tips in mind:

  1. Pre-compile templates: Always parse and compile templates at startup rather than for each request
  2. Cache rendered results: For templates that don't change frequently, consider caching the rendered HTML
  3. Monitor memory usage: Different template engines have different memory footprints
  4. Choose wisely: Don't use multiple engines just because you can. Each engine adds complexity

Summary

Echo's flexible renderer interface makes it possible to use multiple template engines in a single application. This approach provides several benefits:

  • Flexibility to choose the most appropriate template engine for different parts of your application
  • Ability to gradually migrate from one template engine to another
  • Support for specialized template engines for different content types
  • Possibility to implement A/B testing with different template implementations

By implementing a custom multi-renderer that delegates to specific template engines, you can take full advantage of Echo's rendering capabilities while maintaining a clean and maintainable codebase.

Additional Resources

Exercises

  1. Implement a multi-renderer that supports at least three different template engines
  2. Create a system that automatically selects the template engine based on file extension
  3. Implement template caching to improve performance
  4. Build a simple A/B testing framework using multiple template engines
  5. Create a migration tool that helps convert templates from one engine format to another


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