Skip to main content
The view engine is the core of Mizu’s template system. This guide covers all configuration options and how to use them effectively.

Creating an Engine

Create a view engine with view.New():
import "github.com/go-mizu/mizu/view"

engine := view.New(view.Config{
    Dir:           "./views",
    Extension:     ".html",
    DefaultLayout: "default",
    Development:   true,
})

Configuration Options

Dir

The directory where your templates are stored.
view.Config{
    Dir: "./views",  // Default: "views"
}
The engine expects this structure inside the directory:
views/
├── layouts/       # Layout templates
│   └── default.html
└── pages/         # Page templates
    ├── home.html
    └── about.html

FS

An fs.FS filesystem to load templates from. Use this with embed.FS in production to bundle templates into your binary.
import "embed"

//go:embed views
var viewsFS embed.FS

engine := view.New(view.Config{
    FS: viewsFS,  // Use embedded filesystem
})
When FS is set, templates are loaded from the embedded filesystem instead of the local filesystem. The directory structure inside the embedded FS should match what you’d have on disk.

Extension

The file extension for template files.
view.Config{
    Extension: ".html",  // Default: ".html"
}
You can use other extensions:
view.Config{
    Extension: ".tmpl",  // Now looks for .tmpl files
}

DefaultLayout

The layout template to use when rendering pages. This refers to a file in the layouts/ directory.
view.Config{
    DefaultLayout: "default",  // Uses layouts/default.html
}
You can override this per-render with view.Layout() or disable it with view.NoLayout().

Delims

Custom template delimiters. Useful if your templates contain {{ and }} literally (common with Vue.js or Angular templates).
view.Config{
    Delims: [2]string{"<%", "%>"},  // Default: ["{{", "}}"]
}
Now templates use <% %> instead:
<h1>Hello, <%.Data.Name%></h1>

Development

Enables development mode features:
  • Template reload - Templates are re-read from disk on every request
  • No caching - Templates are never cached
view.Config{
    Development: true,  // Default: false
}
Important: Never enable Development mode in production. It significantly impacts performance. A common pattern is to use an environment variable:
view.Config{
    Development: os.Getenv("ENV") != "production",
}

Funcs

Custom template functions to add to all templates.
import "html/template"
import "strings"

view.Config{
    Funcs: template.FuncMap{
        "uppercase": strings.ToUpper,
        "formatDate": func(t time.Time) string {
            return t.Format("Jan 2, 2006")
        },
    },
}
Now you can use these in templates:
<h1>{{uppercase .Data.Name}}</h1>
<p>Posted on {{formatDate .Data.CreatedAt}}</p>

Built-in Functions

The view engine provides these functions by default:
FunctionDescriptionExample
dictCreate a map{{dict "key" "value"}}
listCreate a slice{{list 1 2 3}}
upperUppercase string{{upper .Data.Name}}
lowerLowercase string{{lower .Data.Name}}
trimTrim whitespace{{trim .Data.Text}}
containsCheck substring{{if contains .Data.Text "hello"}}
replaceReplace substring{{replace .Data.Text "old" "new"}}
splitSplit string{{split .Data.Tags ","}}
joinJoin slice{{join .Data.Items ", "}}
hasPrefixCheck prefix{{if hasPrefix .Data.URL "https"}}
hasSuffixCheck suffix{{if hasSuffix .Data.File ".go"}}

Engine Methods

New

Creates a new view engine with the given configuration.
engine := view.New(view.Config{
    Dir:         "./views",
    Development: true,
})

Load

Loads and validates all templates at startup. Call this in production to fail fast if templates have errors.
engine := view.New(config)

if err := engine.Load(); err != nil {
    log.Fatal("template error:", err)
}
This loads all files in layouts/ and pages/ directories.

Clear

Clears the template cache. Useful if you need to reload templates without restarting.
engine.Clear()

Render

Renders a page template. The first argument is an io.Writer (like http.ResponseWriter), followed by the template name, data, and optional render options.
err := engine.Render(w, "home", view.Data{
    "Title": "Home",
    "User":  currentUser,
})
The template name refers to a file in pages/ without the extension.

Middleware

Returns a Mizu middleware that adds the engine to every request context.
app := mizu.New()
app.Use(engine.Middleware())
After adding this middleware, you can use view.From(c) and view.Render(c, ...) in handlers.

Package Functions

From

Retrieves the engine from a request context. Use this in handlers after adding the middleware.
func handler(c *mizu.Ctx) error {
    engine := view.From(c)
    if engine == nil {
        return errors.New("view engine not configured")
    }
    return engine.Render(c.Writer(), "page", data)
}

Render (package-level)

A convenience function that gets the engine from context and renders. This is the most common way to render templates.
func handler(c *mizu.Ctx) error {
    return view.Render(c, "home", view.Data{
        "Title": "Home",
    })
}
This is equivalent to:
engine := view.From(c)
c.Writer().Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer().WriteHeader(200)
return engine.Render(c.Writer(), "home", data)

Render Options

When calling Render(), you can pass options to customize behavior.

Layout

Override the default layout for this render:
view.Render(c, "dashboard", data, view.Layout("admin"))
This uses layouts/admin.html instead of the default layout.

NoLayout

Render without any layout (useful for partial responses):
view.Render(c, "user-row", data, view.NoLayout())

Template Data Structure

When you render a template, your data is wrapped in a structure:
type pageData struct {
    Page    pageMeta      // Template metadata
    Data    any           // Your data
    Content template.HTML // Rendered page (only in layouts)
}

type pageMeta struct {
    Name   string  // Template name (e.g., "home")
    Layout string  // Layout name (e.g., "default")
}
In page templates:
<h1>{{.Data.Title}}</h1>
<p>Template: {{.Page.Name}}</p>
In layout templates:
<title>{{.Data.Title}}</title>
<body>
    {{.Content}}  <!-- Rendered page content -->
</body>

Complete Example

Here’s a typical configuration for a production app:
package main

import (
    "embed"
    "html/template"
    "os"
    "strings"
    "time"

    "github.com/go-mizu/mizu"
    "github.com/go-mizu/mizu/view"
)

//go:embed views
var viewsFS embed.FS

func main() {
    isDev := os.Getenv("ENV") != "production"

    config := view.Config{
        Extension:     ".html",
        DefaultLayout: "default",
        Development:   isDev,
        Funcs: template.FuncMap{
            "formatDate": formatDate,
            "truncate":   truncate,
        },
    }

    if isDev {
        // Development: load from filesystem
        config.Dir = "./views"
    } else {
        // Production: load from embedded FS
        config.FS = viewsFS
    }

    engine := view.New(config)

    // Preload templates in production to fail fast
    if !isDev {
        if err := engine.Load(); err != nil {
            panic(err)
        }
    }

    app := mizu.New()
    app.Use(engine.Middleware())

    app.Get("/", homeHandler)
    app.Get("/about", aboutHandler)

    app.Listen(":8080")
}

func formatDate(t time.Time) string {
    return t.Format("January 2, 2006")
}

func truncate(s string, length int) string {
    if len(s) <= length {
        return s
    }
    return s[:length] + "..."
}

func homeHandler(c *mizu.Ctx) error {
    return view.Render(c, "home", view.Data{
        "Title": "Welcome",
    })
}

func aboutHandler(c *mizu.Ctx) error {
    return view.Render(c, "about", view.Data{
        "Title": "About Us",
    })
}

Error Handling

Template Errors

The engine returns an Error type that wraps template parsing and execution errors:
err := engine.Render(w, "page", data)
if err != nil {
    var viewErr *view.Error
    if errors.As(err, &viewErr) {
        log.Printf("Template error (%s %q): %v",
            viewErr.Kind, viewErr.Name, viewErr.Err)
    }
}
The Error type has:
  • Kind - “page”, “layout”, or “template”
  • Name - Template name
  • Err - Underlying error (nil if not found)

ErrNotFound

Returned when a template file doesn’t exist:
if errors.Is(err, view.ErrNotFound) {
    // Template file not found
}

Best Practices

1. Use Development Mode Locally

Always enable development mode during development:
Development: os.Getenv("ENV") != "production"

2. Load Templates in Production

Call Load() at startup to catch template errors early:
if err := engine.Load(); err != nil {
    log.Fatal(err)
}

3. Embed Templates for Production

Use embed.FS so templates are bundled in your binary:
//go:embed views
var viewsFS embed.FS
This eliminates the need to deploy template files alongside your binary.

4. Keep Custom Functions Pure

Custom functions should be pure (no side effects):
// Good: pure function
"uppercase": strings.ToUpper,

// Bad: has side effects
"log": func(s string) string {
    log.Println(s)  // Side effect!
    return s
},

5. Use Consistent Data Structures

Create struct types for complex template data:
type PageData struct {
    Title string
    User  *User
    Posts []Post
}

view.Render(c, "home", PageData{
    Title: "Home",
    User:  currentUser,
    Posts: posts,
})
Note: When using a struct instead of view.Data (which is map[string]any), access it directly in templates:
<h1>{{.Data.Title}}</h1>  <!-- With view.Data map -->
<h1>{{.Data.Title}}</h1>  <!-- With struct - same access pattern -->