Skip to main content

Templating

Okapi supports template rendering for serving HTML pages. You can use the built-in renderer, Go's standard html/template package, or any custom renderer that implements Okapi's Renderer interface.

HTML Escaping​

The built-in renderer — okapi.Template, behind NewTemplate, NewTemplateFromFiles, NewTemplateFromDirectory, NewTemplateWithConfig and c.Render() — is built on html/template and applies its contextual auto-escaping. Values interpolated into a template are escaped for the context they land in: HTML body, attribute, URL, JavaScript, CSS.

A template that must emit markup verbatim has to say so explicitly, by passing a template.HTML (or template.JS, template.URL) value:

return c.Render(http.StatusOK, "post.html", okapi.M{
"title": post.Title, // escaped
"body": template.HTML(post.SafeHTML), // emitted as-is
})

This is a change in behaviour: earlier releases used text/template, which performs no escaping at all.

How Templates Are Named​

NewTemplateFromDirectory, NewTemplateFromFiles and NewTemplate load files through html/template's ParseFiles, ParseGlob and ParseFS. Those name each template after the file's base name, extension included — hello.html, not hello. So:

// public/views/hello.html, with no {{define}} block
return c.Render(http.StatusOK, "hello.html", data)

A file that wraps its content in {{define "name"}}…{{end}} registers that name instead, which is how you get a short name:

<!-- public/views/hello.html -->
{{define "hello"}}
<h1>Hello</h1>
{{end}}
return c.Render(http.StatusOK, "hello", data)

Pick one convention and stay with it; c.Render returns the html/template error when the name is not defined.

Built-in Template Renderer​

The simplest way to get started. NewTemplateFromDirectory scans a folder for files matching the given extensions and registers each as a named template.

func main() {
// Load all .html and .tmpl files from the views directory
tmpl, err := okapi.NewTemplateFromDirectory("public/views", ".html", ".tmpl")
if err != nil {
log.Fatal(err)
}

o := okapi.Default().WithRenderer(tmpl)

o.Get("/", func(c *okapi.Context) error {
return c.Render(http.StatusOK, "hello.html", okapi.M{
"title": "Greeting Page",
"message": "Hello, World!",
})
})

if err := o.Start(); err != nil {
log.Fatal(err)
}
}

When no extensions are passed, .html and .tmpl are used. The function returns an error when the directory contains no templates.

You can also load templates from a glob pattern instead of a directory:

tmpl, err := okapi.NewTemplateFromFiles("public/views/*.html")

Embedded Templates​

For production deployments, embedding templates directly into the binary using Go's embed package eliminates runtime file-system dependencies.

var (
//go:embed views/*
Views embed.FS

AssetsFS = http.FS(must(fs.Sub(Views, "views/assets")))
)

func main() {
app := okapi.New()
app.WithRendererFromFS(Views, "views/*.html")

app.Get("/", func(c *okapi.Context) error {
return c.Render(http.StatusOK, "home.html", okapi.M{
"title": "Go Okapi Bookstore",
"headline": "Discover your next great read",
"books": books,
})
})

// Serve embedded static assets
app.StaticFS("/assets", AssetsFS)

if err := app.Start(); err != nil {
panic(err)
}
}

func must(fsys fs.FS, err error) fs.FS {
if err != nil {
panic(err)
}
return fsys
}

Custom Renderers​

If the built-in renderer doesn't fit your needs — for example, you want to use a third-party templating engine like Sprig or Petal — you can supply your own. Okapi supports two approaches.

The Renderer interface is:

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

Renderer Function​

Use RendererFunc for lightweight, one-off renderers without needing a full struct. Attach it with WithRenderer — there is no exported Renderer field on *Okapi:

o.WithRenderer(okapi.RendererFunc(func(w io.Writer, name string, data interface{}, c *okapi.Context) error {
tmpl, err := template.ParseFiles("templates/" + name + ".html")
if err != nil {
return err
}
return tmpl.ExecuteTemplate(w, name, data)
}))

Warning: This example re-parses the template file on every request, and it builds a filesystem path from the name it is given — never pass a name derived from a request to it. In production, parse templates once at startup and cache the result (see the struct-based example below).

Struct-Based Renderer​

For more control — caching, shared state, or preloaded templates — implement the Renderer interface on a struct:

type Template struct {
templates *template.Template
}

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

// At startup:
tmpl := &Template{
templates: template.Must(template.ParseGlob("templates/*.html")),
}

o := okapi.New().WithRenderer(tmpl)

Templates are parsed once via ParseGlob and reused for every request, which is the recommended approach for production.


Rendering a View​

Once a renderer is attached, use c.Render inside any handler to render a named template with arbitrary data:

o.Get("/welcome", func(c *okapi.Context) error {
return c.Render(http.StatusOK, "welcome", okapi.M{
"title": "Welcome Page",
"message": "Hello from Okapi!",
})
})

Without a renderer attached, c.Render returns okapi.ErrNoRenderer.

okapi.M is a shorthand for map[string]any. Each key becomes accessible inside the template as {{.keyName}}.

Example Template​

templates/welcome.html:

{{define "welcome"}}
<!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>{{.message}}</h1>
</body>
</html>
{{end}}

Because this file defines welcome, c.Render(..., "welcome", ...) finds it.


Static File Serving​

Okapi can serve static assets alongside your rendered pages.

Serve a directory​

All files under public/assets become accessible at /static/*:

o.Static("/static", "public/assets")

Directory listings are not served: a request for a directory answers 404.

Serve a single file​

o.StaticFile("/favicon.ico", "public/favicon.ico")

Or from a handler — ServeFile writes the response itself and returns nothing:

o.Get("/favicon.ico", func(c *okapi.Context) error {
c.ServeFile("public/favicon.ico")
return nil
})

For a file name that comes from the request, use c.ServeFileFrom(root, name), which refuses .. and cannot escape root.

Serve an embedded filesystem​

Use StaticFS to serve assets that were compiled into the binary via embed:

app.StaticFS("/assets", AssetsFS)

This pairs naturally with the Embedded Templates example above, where AssetsFS is derived from the same embed.FS.

Serve a single-page application​

To serve a client-side routed app (React, Vue, …) with index fallback, use Web / WebFS instead of Static. See Single-Page Applications.