Skip to main content

Gin Custom Template Engine

The Gin framework offers great flexibility when it comes to rendering templates. While Gin uses Go's standard html/template package by default, you might want to use a different template engine that better suits your needs. This guide will show you how to implement and use custom template engines in your Gin web applications.

Introduction to Custom Template Engines

A template engine allows you to separate your application's logic from the presentation layer. By default, Gin uses Go's built-in template system, but sometimes you might need features that aren't available in the standard package or prefer the syntax of another template engine.

Some reasons to use a custom template engine include:

  • More advanced templating features
  • Different syntax that might be more familiar to your team
  • Better integration with frontend frameworks
  • Improved performance for specific use cases
  • Special rendering capabilities like markdown or email templates

Understanding the HTMLRenderer Interface

To implement a custom template engine in Gin, you need to understand the HTMLRenderer interface:

go
type HTMLRenderer interface {
Instance(string, interface{}) render.Render
}

Any template engine you want to use with Gin must implement this interface. The Instance method takes a template name and data, returning a render.Render object responsible for rendering the template.

Creating a Custom Template Engine

Let's create a simple custom template engine that uses a different template library. In this example, we'll implement an adapter for the popular Pongo2 template engine, which offers a Django/Jinja2-like syntax.

Step 1: Install the Required Packages

First, let's install the necessary packages:

bash
go get github.com/gin-gonic/gin
go get github.com/flosch/pongo2/v4

Step 2: Create the Template Engine Adapter

Now, let's create our Pongo2 template engine adapter:

go
package main

import (
"net/http"

"github.com/flosch/pongo2/v4"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
)

type Pongo2Renderer struct {
TemplateDir string
TemplateSet *pongo2.TemplateSet
}

type Pongo2Render struct {
Template *pongo2.Template
Context pongo2.Context
}

// Instance implements the HTMLRenderer interface
func (p *Pongo2Renderer) Instance(name string, data interface{}) render.Render {
tmpl, err := p.TemplateSet.FromCache(name)
if err != nil {
panic(err)
}

return &Pongo2Render{
Template: tmpl,
Context: data.(pongo2.Context),
}
}

// Render implements the Render interface
func (p *Pongo2Render) Render(w http.ResponseWriter) error {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
return p.Template.ExecuteWriter(p.Context, w)
}

// WriteContentType writes the content type header
func (p *Pongo2Render) WriteContentType(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}

// Create a new Pongo2 renderer with the given template directory
func NewPongo2Renderer(templateDir string) *Pongo2Renderer {
loader := pongo2.MustNewLocalFileSystemLoader(templateDir)
templateSet := pongo2.NewSet("default", loader)

return &Pongo2Renderer{
TemplateDir: templateDir,
TemplateSet: templateSet,
}
}

Step 3: Configure Gin to Use the Custom Template Engine

Now, let's set up a Gin application that uses our custom Pongo2 template engine:

go
package main

import (
"github.com/flosch/pongo2/v4"
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

// Create a new Pongo2 renderer
renderer := NewPongo2Renderer("templates")

// Set Gin's HTMLRenderer to our custom renderer
router.HTMLRender = renderer

// Define a route that renders a template
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", pongo2.Context{
"title": "Custom Template Engine",
"message": "Hello from Pongo2!",
})
})

router.Run(":8080")
}

Step 4: Create a Sample Template

Now, create a template file in the templates directory named index.html:

html
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>{{ message }}</p>

{# Pongo2 supports advanced features like loops #}
<ul>
{% for i in range(1, 6) %}
<li>Item {{ i }}</li>
{% endfor %}
</ul>
</body>
</html>

Output

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

Custom Template Engine

Hello from Pongo2!

• Item 1
• Item 2
• Item 3
• Item 4
• Item 5

Real-World Example: Markdown Template Engine

Let's create another practical example: a Markdown template engine that renders Markdown content as HTML. This can be useful for documentation sites or blogs.

Step 1: Install Markdown Package

bash
go get github.com/russross/blackfriday/v2

Step 2: Create the Markdown Template Engine

go
package main

import (
"io/ioutil"
"net/http"
"path/filepath"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
"github.com/russross/blackfriday/v2"
)

type MarkdownRenderer struct {
TemplateDir string
}

type MarkdownRender struct {
Template string
Data interface{}
}

// Instance implements the HTMLRenderer interface
func (m *MarkdownRenderer) Instance(name string, data interface{}) render.Render {
// Read the markdown file
content, err := ioutil.ReadFile(filepath.Join(m.TemplateDir, name))
if err != nil {
panic(err)
}

return &MarkdownRender{
Template: string(content),
Data: data,
}
}

// Render implements the Render interface
func (m *MarkdownRender) Render(w http.ResponseWriter) error {
// Convert markdown to HTML
output := blackfriday.Run([]byte(m.Template))

w.Write(output)
return nil
}

// WriteContentType writes the content type header
func (m *MarkdownRender) WriteContentType(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}

// Create a new Markdown renderer with the given template directory
func NewMarkdownRenderer(templateDir string) *MarkdownRenderer {
return &MarkdownRenderer{
TemplateDir: templateDir,
}
}

Step 3: Use the Markdown Template Engine in a Gin Application

go
package main

import (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

// Create a new Markdown renderer
renderer := NewMarkdownRenderer("markdown")

// Set Gin's HTMLRenderer to our custom renderer
router.HTMLRender = renderer

// Define a route that renders a markdown file
router.GET("/docs/:page", func(c *gin.Context) {
page := c.Param("page")
c.HTML(200, page+".md", nil)
})

router.Run(":8080")
}

Step 4: Create a Sample Markdown File

Create a markdown file in the markdown directory named getting-started.md:

markdown
# Getting Started

## Introduction

This is a **Markdown** document that will be rendered as HTML.

## Features

- Easy to write
- Easy to read
- Supports *formatting*

## Code Examples

```go
func HelloWorld() {
fmt.Println("Hello, World!")
}

When you navigate to `http://localhost:8080/docs/getting-started`, you'll see the Markdown content rendered as HTML.

## Advanced Implementation: Template with Layout Support

One common requirement in web applications is to have a consistent layout across different pages. Let's create a custom template engine that supports layouts.

### Step 1: Create a Template Engine with Layout Support

```go
package main

import (
"bytes"
"html/template"
"net/http"
"path/filepath"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
)

type LayoutRenderer struct {
TemplateDir string
Templates *template.Template
Layout string
TemplateCache map[string]*template.Template
}

type LayoutRender struct {
Template *template.Template
Layout string
Name string
Data interface{}
}

// Instance implements the HTMLRenderer interface
func (l *LayoutRenderer) Instance(name string, data interface{}) render.Render {
return &LayoutRender{
Template: l.Templates,
Layout: l.Layout,
Name: name,
Data: data,
}
}

// Render implements the Render interface
func (l *LayoutRender) Render(w http.ResponseWriter) error {
// Create a buffer to capture the output
buf := bytes.NewBuffer(nil)

// Render the content template first
err := l.Template.ExecuteTemplate(buf, l.Name, l.Data)
if err != nil {
return err
}

// Add the content to the data for the layout
data := map[string]interface{}{
"Content": template.HTML(buf.String()),
}

// If original data is a map, merge it with the content
if dataMap, ok := l.Data.(map[string]interface{}); ok {
for k, v := range dataMap {
data[k] = v
}
}

// Render the layout with the content inside
return l.Template.ExecuteTemplate(w, l.Layout, data)
}

// WriteContentType writes the content type header
func (l *LayoutRender) WriteContentType(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}

// Create a new layout renderer
func NewLayoutRenderer(templateDir, layout string) *LayoutRenderer {
templates := template.Must(template.ParseGlob(filepath.Join(templateDir, "*.html")))

return &LayoutRenderer{
TemplateDir: templateDir,
Templates: templates,
Layout: layout,
}
}

Step 2: Use the Layout Template Engine in a Gin Application

go
package main

import (
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()

// Create a new layout renderer
renderer := NewLayoutRenderer("templates", "layout.html")

// Set Gin's HTMLRenderer to our custom renderer
router.HTMLRender = renderer

// Define routes that render templates
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", map[string]interface{}{
"title": "Home Page",
"message": "Welcome to our website!",
})
})

router.GET("/about", func(c *gin.Context) {
c.HTML(200, "about.html", map[string]interface{}{
"title": "About Us",
"message": "Learn more about our company.",
})
})

router.Run(":8080")
}

Step 3: Create the Layout and Content Templates

Create a layout template in templates/layout.html:

html
<!DOCTYPE html>
<html>
<head>
<title>{{ .title }}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>

<main>
<!-- Content template will be inserted here -->
{{ .Content }}
</main>

<footer>
<p>&copy; 2023 My Website</p>
</footer>
</body>
</html>

Create a content template in templates/index.html:

html
<div class="home-page">
<h1>{{ .title }}</h1>
<p>{{ .message }}</p>
<div class="features">
<div class="feature">Feature 1</div>
<div class="feature">Feature 2</div>
<div class="feature">Feature 3</div>
</div>
</div>

Create another content template in templates/about.html:

html
<div class="about-page">
<h1>{{ .title }}</h1>
<p>{{ .message }}</p>
<div class="team">
<div class="member">Team Member 1</div>
<div class="member">Team Member 2</div>
</div>
</div>

With this setup, both pages will use the same layout, but with different content. When you navigate to different routes, the correct content will be inserted into the layout.

Summary

Custom template engines in Gin provide flexibility and power for rendering views in your web applications. By implementing the HTMLRenderer interface, you can integrate almost any template library or create specialized rendering solutions for specific needs.

In this guide, we explored several custom template engines:

  1. A Pongo2 Adapter for Django/Jinja2-like syntax
  2. A Markdown Renderer for documentation and content-heavy sites
  3. A Layout Engine for maintaining consistent page structures

When choosing or creating a template engine, consider the specific needs of your project, the familiarity of your team with different templating syntaxes, and the performance requirements of your application.

Additional Resources and Exercises

Resources

Exercises

  1. Exercise: Modify the Markdown renderer to support template variables by preprocessing the markdown content before rendering.

  2. Exercise: Implement a template engine that uses handlebars.js syntax with the raymond Go library.

  3. Exercise: Create a template engine that renders React components server-side using Go's V8 bindings.

  4. Exercise: Add caching to the custom template engines to improve performance in production environments.

  5. Exercise: Extend the layout renderer to support nested layouts (e.g., a base layout with different sub-layouts for different sections of your site).

By mastering custom template engines in Gin, you can create more flexible, maintainable, and efficient web applications while maintaining separation between your application logic and presentation.



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