Skip to main content
This guide covers everything you need to deploy your Mizu views in production: embedding templates, caching, performance optimization, and best practices.

Embedding Templates

In production, embed templates into your binary. This eliminates file system dependencies and ensures templates can’t be accidentally modified.

Using embed.FS

Go’s embed package lets you include files in your binary:
package main

import (
    "embed"

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

//go:embed views
var viewsFS embed.FS

func main() {
    engine := view.New(view.Config{
        FS:            viewsFS,    // Use embedded filesystem
        Extension:     ".html",
        DefaultLayout: "default",
    })

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

    // ... routes ...

    app.Listen(":8080")
}

Directory Structure for Embedding

The //go:embed directive embeds a directory relative to the source file:
myapp/
β”œβ”€β”€ main.go           # Contains //go:embed views
β”œβ”€β”€ views/
β”‚   β”œβ”€β”€ layouts/
β”‚   β”‚   └── default.html
β”‚   └── pages/
β”‚       β”œβ”€β”€ home.html
β”‚       └── about.html
└── go.mod
After building, the binary contains all templates.

Conditional Embedding

Use environment variables to switch between development and production:
package main

import (
    "embed"
    "os"

    "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,
    }

    if isDev {
        // Development: filesystem for hot reload
        config.Dir = "./views"
    } else {
        // Production: embedded with caching
        config.FS = viewsFS
    }

    engine := view.New(config)

    // ... rest of application
}

Preloading Templates

In production, preload all templates at startup to:
  1. Fail fast - Catch template errors before serving requests
  2. Warm cache - Avoid first-request latency
  3. Validate - Ensure all templates parse correctly
engine := view.New(view.Config{
    FS:            viewsFS,
    DefaultLayout: "default",
})

// Preload and validate all templates
if err := engine.Load(); err != nil {
    log.Fatal("Template error:", err)
}

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

Preload Errors

Load() catches errors like:
  • Missing template files
  • Template syntax errors
  • Invalid template references
err := engine.Load()
if err != nil {
    // err contains details about the failure
    log.Fatal(err)
}

Performance Optimization

Template Caching

In production mode (Development: false), templates are cached:
  • Parsed once - Templates are parsed at first use or Load()
  • Reused - Same parsed template serves all requests
  • No disk I/O - Templates read from memory
view.Config{
    Development: false,  // Enable caching (default)
}

Pre-compute in Handlers

Move complex logic to Go:
// Good: compute once in handler
func handler(c *mizu.Ctx) error {
    posts := fetchPosts()

    // Pre-compute derived data
    featured := filterFeatured(posts)
    byCategory := groupByCategory(posts)

    return view.Render(c, "posts", view.Data{
        "Posts":      posts,
        "Featured":   featured,
        "ByCategory": byCategory,
    })
}
<!-- Simple template: just displays pre-computed data -->
{{range .Data.Featured}}
    <article>{{.Title}}</article>
{{end}}

Minimize Template Complexity

Complex templates are slower. Keep them focused:
<!-- Avoid: deeply nested conditionals -->
{{if .Data.A}}
    {{if .Data.B}}
        {{if .Data.C}}
            ...
        {{end}}
    {{end}}
{{end}}

<!-- Better: compute in Go, pass simple flag -->
{{if .Data.ShowSpecialContent}}
    ...
{{end}}

Error Handling

Custom Error Pages

Create error page templates:
<!-- views/pages/404.html -->
<div class="error-page">
    <h1>404</h1>
    <p>The page you're looking for doesn't exist.</p>
    <a href="/">Go Home</a>
</div>
<!-- views/pages/500.html -->
<div class="error-page">
    <h1>500</h1>
    <p>Something went wrong on our end.</p>
    <p>Please try again later.</p>
</div>

Error Handler

Set up a global error handler:
app := mizu.New()

app.ErrorHandler = func(c *mizu.Ctx, err error) {
    code := 500
    page := "500"

    // Check for HTTP errors
    var httpErr *mizu.Error
    if errors.As(err, &httpErr) {
        code = httpErr.Code
        if code == 404 {
            page = "404"
        }
    }

    // Log server errors
    if code >= 500 {
        log.Printf("Server error: %v", err)
    }

    // Render error page
    c.Status(code)
    if renderErr := view.Render(c, page, view.Data{
        "Title": "Error",
        "Error": err,
    }); renderErr != nil {
        // Fallback if error page fails
        c.Text("An error occurred")
    }
}

Template Error Handling

Handle template rendering errors gracefully:
func handler(c *mizu.Ctx) error {
    err := view.Render(c, "page", data)
    if err != nil {
        // Log the error
        log.Printf("Template error: %v", err)

        // Return generic error
        return c.Status(500).Text("Internal error")
    }
    return nil
}

Security

Content Security Policy

Set appropriate headers:
app.Use(func(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        c.Set("Content-Security-Policy",
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
        return next(c)
    }
})

HTML Escaping

Go templates escape HTML by default. Only bypass escaping for trusted content:
<!-- Safe: auto-escaped -->
{{.Data.UserComment}}

<!-- Only for trusted content -->
{{.Data.TrustedHTML}}
import "html/template"

view.Data{
    "TrustedHTML": template.HTML(trustedContent),
}

CSRF Tokens

Include CSRF tokens in forms:
<form method="POST" action="/submit">
    <input type="hidden" name="csrf_token" value="{{.Data.CSRFToken}}">
    <!-- form fields -->
</form>

Monitoring

Template Metrics

Track template rendering performance:
func handler(c *mizu.Ctx) error {
    start := time.Now()

    err := view.Render(c, "page", data)

    duration := time.Since(start)
    metrics.RecordTemplateRender("page", duration)

    return err
}

Health Checks

Verify templates work in health checks:
app.Get("/health", func(c *mizu.Ctx) error {
    // Try rendering a simple template
    engine := view.From(c)
    var buf bytes.Buffer
    err := engine.Render(&buf, "health-check", nil, view.NoLayout())
    if err != nil {
        return c.Status(500).JSON(map[string]string{
            "status": "unhealthy",
            "error":  err.Error(),
        })
    }

    return c.JSON(map[string]string{
        "status": "healthy",
    })
})

Deployment Checklist

Before Deploying

  • Set Development: false
  • Use embed.FS for templates
  • Call Load() at startup
  • Create error page templates
  • Set up error handler
  • Test all pages render correctly

Environment Variables

# Production
ENV=production
PORT=8080

# Development
ENV=development
PORT=8080

Docker Example

FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .

# Templates are embedded - no need to copy views/

ENV ENV=production
EXPOSE 8080
CMD ["./server"]

Complete Production Setup

package main

import (
    "embed"
    "errors"
    "log"
    "os"

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

//go:embed views
var viewsFS embed.FS

func main() {
    // Configuration
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    isDev := os.Getenv("ENV") != "production"

    // Create view engine
    config := view.Config{
        Extension:     ".html",
        DefaultLayout: "default",
        Development:   isDev,
    }

    if isDev {
        config.Dir = "./views"
    } else {
        config.FS = viewsFS
    }

    engine := view.New(config)

    // Preload in production
    if !isDev {
        if err := engine.Load(); err != nil {
            log.Fatal("Template error:", err)
        }
        log.Println("Templates loaded successfully")
    }

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

    // Error handler
    app.ErrorHandler = func(c *mizu.Ctx, err error) {
        code := 500
        page := "500"

        var httpErr *mizu.Error
        if errors.As(err, &httpErr) {
            code = httpErr.Code
            if code == 404 {
                page = "404"
            }
        }

        if code >= 500 {
            log.Printf("Error: %v", err)
        }

        c.Status(code)
        view.Render(c, page, view.Data{"Title": "Error"})
    }

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

    // Start server
    log.Printf("Server starting on :%s (dev=%v)", port, isDev)
    if err := app.Listen(":" + port); err != nil {
        log.Fatal(err)
    }
}

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",
    })
}

What’s Next?

Now that your views are ready for production, explore: