Using Scriggo Templates with the Go Fiber web framework
Fiber is an express-inspired framework for Go. It uses fasthttp under the hood and has support for many features like middleware, JSON, and template engines among others.
The Fiber project has support for a lot of template engines but they don't have one for Scriggo. I like Scriggo because it looks a bit like Jinja (Python), Twig (PHP) and Pebble (Java) template engines which I have used extensively recently.
Fortunately, Fiber allows you to implement your own Template Engine without much hassle. In this article I will share a simple implementation of a simple Scriggo template engine that you can use with Fiber.
A Simple Server side rendered site in Go
We are going to create a simple server side rendered site with Go using the Scriggo engine. So first of all create a new project, e.g.:
$ mkdir example-site
$ cd example-site
$ go mod init example.com/site
Copy the code for the engine (find it at the bottom of this article) into a file named scriggo_engine/scriggo_engine.go
so you can import the Engine.
Now, place the following server code in a file named main.go
in your module directory:
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"example.com/site/scriggo_engine"
)
type Server struct {
app *fiber.App
}
func (s *Server) NotImplemented(ctx *fiber.Ctx) error {
return nil
}
func NewServer() *Server {
engine := scriggo_engine.New("templates", ".html")
s := &Server{
app: fiber.New(fiber.Config{
Views: engine,
}),
}
s.app.Get("/", s.indexPage)
return s
}
type Contact struct {
Name string
Phone string
Email string
}
func (s *Server) indexPage(ctx *fiber.Ctx) error {
contacts := []Contact{
{Name: "John Phiri", Email: "john@example.com", Phone: "(+265) 999 123 456"},
{Name: "Mary Phiri", Email: "mary@example.com", Phone: "(+265) 999 123 456"},
{Name: "Jane Phiri", Email: "jane@example.com", Phone: "(+265) 999 123 456"},
}
return ctx.Render("index", fiber.Map{
"contacts": &contacts,
})
}
func (s *Server) Start(bind string) error {
return s.app.Listen(bind)
}
func main() {
server := NewServer()
err := server.Start("localhost:3000")
if err != nil {
log.Fatalf("Failed to run, got error %v", err)
}
}
In a file named templates/base.html
<!DOCTYPE html>
<html lang="en">
<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">
<title>{{ Title() }} - My Website</title>
</head>
<body>
<nav>
<h1>My Website</h1>
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
{{ Content() }}
{{ Footer() }}
</body>
</html>
In a file named templates/index.html
{% extends "base.html" %}
{% macro Title %}Home{% end %}
{% macro Content %}
<section id="app-content">
<div class="contacts">
{% for contact in contacts %}
<li><a href="/contact/{{ contact.Name }}">{{ contact.Phone}}</a> ({{ contact.Email }})</li>
{% end %}
</div>
</section>
{% end %}
{% macro Footer %}
<footer>(c) My Website</footer>
{% end %}
Project Structure
Your directory should look something like this
example-site
├───main.go
├───go.mod
├───go.sum
├───scriggo_engine
| └───scriggo_engine.go
└───templates
├───base.html
└───index.html
Running the code
First we are going to make sure go modules are tidied and downloaded via go mod tidy
and then we can run the main.go
$ go mod tidy
$ go run main.go
Now go to http://localhost:3000
you should see a server-side rendered HTML page!
Registering Scriggo in-built functions
Typically in template engines we may want to call some functions on our data. Scriggo comes with a lot of built-in functions and using them is relatively straight-foward.
Register the function via addFunc
import "github.com/open2b/scriggo/builtin"
// ..
engine.AddFunc("base64", builtin.Base64 )
// ..
Use the function in your templates
<div>{{ base64("Hello") }}</div>
Scriggo Template Engine for Fiber
Here is the implementation of the engine code, it has not been battle-tested so if there are bugs or ways to improve it, feel free to reach out on Twitter.
package scriggo_engine
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/utils"
"github.com/open2b/scriggo"
"github.com/open2b/scriggo/native"
)
// Engine struct
type Engine struct {
// views folder
directory string
// http.FileSystem supports embedded files
fileSystem http.FileSystem
// views extension
extension string
// layout variable name that incapsulates the template
layout string
// determines if the engine parsed all templates
loaded bool
// reload on each render
reload bool
// debug prints the parsed templates
debug bool
// lock for funcmap and templates
mutex sync.RWMutex
// template funcmap
funcmap native.Declarations
// templates
templates map[string]string
// scriggo filesystem
fsys scriggo.Files
}
// New returns a Scriggo render engine for Fiber
func New(directory, extension string) *Engine {
engine := &Engine{
directory: directory,
extension: extension,
layout: "embed",
funcmap: make(native.Declarations),
}
engine.AddFunc(engine.layout, func() error {
return fmt.Errorf("layout called unexpectedly.")
})
return engine
}
func NewFileSystem(fs http.FileSystem, extension string) *Engine {
engine := &Engine{
directory: "/",
fileSystem: fs,
extension: extension,
layout: "embed",
funcmap: make(native.Declarations),
}
engine.AddFunc(engine.layout, func() error {
return fmt.Errorf("layout called unexpectedly.")
})
return engine
}
// Layout defines the variable name that will incapsulate the template
func (e *Engine) Layout(key string) *Engine {
e.layout = key
return e
}
// Delims sets the action delimiters to the specified strings, to be used in
// templates. An empty delimiter stands for the
// corresponding default: {{ or }}.
func (e *Engine) Delims(left, right string) *Engine {
fmt.Println("delims: this method is not supported for scriggo")
return e
}
// AddFunc adds the function to the template's function map.
// It is legal to overwrite elements of the default actions
func (e *Engine) AddFunc(name string, fn native.Declaration) *Engine {
e.mutex.Lock()
e.funcmap[name] = fn
e.mutex.Unlock()
return e
}
// Reload if set to true the templates are reloading on each render,
// use it when you're in development and you don't want to restart
// the application when you edit a template file.
func (e *Engine) Reload(enabled bool) *Engine {
e.reload = enabled
return e
}
// Debug will print the parsed templates when Load is triggered.
func (e *Engine) Debug(enabled bool) *Engine {
e.debug = enabled
return e
}
// Load parses the templates to the engine.
func (e *Engine) Load() error {
// race safe
e.mutex.Lock()
defer e.mutex.Unlock()
e.templates = make(map[string]string)
e.fsys = scriggo.Files{}
// Loop trough each directory and register template files
walkFn := func(path string, info os.FileInfo, err error) error {
// Return error if exist
if err != nil {
return err
}
// Skip file if it's a directory or has no file info
if info == nil || info.IsDir() {
return nil
}
// Skip file if it does not equal the given template extension
if len(e.extension) >= len(path) || path[len(path)-len(e.extension):] != e.extension {
return nil
}
// Get the relative file path
// ./views/html/index.tmpl -> index.tmpl
rel, err := filepath.Rel(e.directory, path)
if err != nil {
return err
}
// Reverse slashes '\' -> '/' and
// partials\footer.tmpl -> partials/footer.tmpl
name := filepath.ToSlash(rel)
// Remove ext from name 'index.tmpl' -> 'index'
name = strings.TrimSuffix(name, e.extension)
// name = strings.Replace(name, e.extension, "", -1)
// Read the file
// #gosec G304
buf, err := utils.ReadFile(path, e.fileSystem)
if err != nil {
return err
}
e.fsys[filepath.ToSlash(rel)] = buf
e.templates[name] = filepath.ToSlash(rel)
// Debugging
if e.debug {
fmt.Printf("views: parsed template: %s\n", name)
}
return err
}
// notify engine that we parsed all templates
e.loaded = true
if e.fileSystem != nil {
return utils.Walk(e.fileSystem, e.directory, walkFn)
}
return filepath.Walk(e.directory, walkFn)
}
// Render will execute the template name along with the given values.
func (e *Engine) Render(out io.Writer, template string, binding interface{}, layout ...string) error {
if !e.loaded || e.reload {
if e.reload {
e.loaded = false
}
if err := e.Load(); err != nil {
return err
}
}
templatePath, ok := e.templates[template]
if !ok {
return fmt.Errorf("render: template %s does not exist", template)
}
opts := &scriggo.BuildOptions{
Globals: e.funcmap,
}
// Register the variables as Globals for Scriggo's type checking to work
for key, value := range binding.(fiber.Map) {
opts.Globals[key] = value
}
// Build the template.
tmpl, err := scriggo.BuildTemplate(e.fsys, templatePath, opts)
if err != nil {
return err
}
return tmpl.Run(out, nil, nil)
}
What's next?
Support for embedded filesytems (
embed.FS
) - I suspect it's already possible but the API doesn't look good for doing so yet.Releasing to the public - I am thinking of either releasing this as a module that others can consume or sending a Pull Request to the Fiber team if they would be interested, or I may just make it a Gist. We will see. Right now the proof of concept works well enough for me to play with on toy projects.
I may update this article later on