GitHub - alesr/templator: A type-safe HTML template rendering engine for Go (original) (raw)

Templator

codecov Go Report Card Go Reference

A type-safe HTML template rendering engine for Go.

Table of Contents

Problem Statement

Go's built-in template package lacks type safety, which can lead to runtime errors when template data doesn't match what the template expects. For example:

// Traditional approach with Go templates tmpl := template.Must(template.ParseFiles("home.html"))

// This compiles but will fail at runtime if the template expects different fields tmpl.Execute(w, struct { WrongField string MissingRequired int }{})

// No compile-time checks for: // - Missing required fields // - Wrong field types // - Typos in field names

Templator solves this by providing compile-time type checking for your templates:

// Define your template data type type HomeData struct { Title string Content string }

// Initialize registry with type parameter reg, _ := templator.NewRegistryHomeData

// Get type-safe handler and execute template home, _ := reg.GetHome() home.Execute(ctx, w, HomeData{ Title: "Welcome", Content: "Hello", })

// Won't compile - wrong data structure home.Execute(ctx, w, struct{ WrongField string }{})

Features

Installation

go install github.com/alesr/templator

Quick Start

package main

import ( "context" "log" "os"

"github.com/alesr/templator"

)

// Define your template data type HomeData struct { Title string Content string }

func main() { // Use the filesystem of your choice fs := os.DirFS(".")

// Initialize registry with your data type
reg, err := templator.NewRegistry[HomeData](fs)
if err != nil {
    log.Fatal(err)
}

// Get type-safe handler for home template
home, err := reg.GetHome()
if err != nil {
    log.Fatal(err)
}

// Execute template with proper data
err = home.Execute(context.Background(), os.Stdout, HomeData{
    Title:   "Welcome",
    Content: "Hello, World!",
})
if err != nil {
    log.Fatal(err)
}

}

Usage Examples

Type-Safe Templates

// Define different data types for different templates type HomeData struct { Title string Content string }

type AboutData struct { Company string Year int }

// Create registries for different template types homeReg := templator.NewRegistryHomeData aboutReg := templator.NewRegistryAboutData

// Get handlers home, _ := homeReg.GetHome() about, _ := aboutReg.GetAbout()

// Type safety enforced at compile time home.Execute(ctx, w, HomeData{...}) // ✅ Compiles home.Execute(ctx, w, AboutData{...}) // ❌ Compile error

Using Template Functions

// Define your custom functions funcMap := template.FuncMap{ "upper": strings.ToUpper, "lower": strings.ToLower, }

// Create registry with functions reg, err := templator.NewRegistry[PageData](fs, templator.WithTemplateFuncsPageData)

// Use functions in your templates: //

{{.Title | upper}}

File System Support

// Embedded FS //go:embed templates/* var embedFS embed.FS reg := templator.NewRegistryHomeData

// OS File System reg := templator.NewRegistryHomeData

// In-Memory (testing) fsys := fstest.MapFS{ "templates/home.html": &fstest.MapFile{ Data: []byte(<h1>{{.Title}}</h1>), }, } reg := templator.NewRegistryHomeData

Field Validation

type ArticleData struct { Title string // Only these two fields Content string // are allowed in templates }

// Enable validation during registry creation reg := templator.NewRegistry[ArticleData]( fs, templator.WithFieldValidation(ArticleData{}), )

// Example templates:

// valid.html: //

{{.Title}}

// ✅ OK - Title exists in ArticleData //

{{.Content}}

// ✅ OK - Content exists in ArticleData

// invalid.html: //

{{.Author}}

// ❌ Error - Author field doesn't exist //

{{.PublishedAt}}

// ❌ Error - PublishedAt field doesn't exist

// Using the templates: handler, err := reg.Get("valid") // ✅ Success - all fields exist if err != nil { log.Fatal(err) }

handler, err := reg.Get("invalid") // ❌ Error: "template 'invalid' validation error: Author - field 'Author' not found in type ArticleData"

// The validation error provides: // - Template name // - Invalid field path // - Detailed error message if validErr, ok := err.(*templator.ValidationError); ok { fmt.Printf("Template: %s\n", validErr.TemplateName) fmt.Printf("Invalid field: %s\n", validErr.FieldPath) fmt.Printf("Error: %v\n", validErr.Err) }

This validation happens when loading the template, not during execution, helping catch field mismatches early in development.

Template Generation

Templates are automatically discovered and type-safe methods are generated:

templates/ ├── home.html -> reg.GetHome() ├── about.html -> reg.GetAbout() └── components/ └── header.html -> reg.GetComponentsHeader()

Generate methods

go generate ./...

The generation process creates a templator_methods.go file containing type-safe method handlers for each template. For example:

// Code generated by go generate; DO NOT EDIT. func (r *Registry[T]) GetHome() (*Handler[T], error) { return r.Get("home") }

func (r *Registry[T]) GetAbout() (*Handler[T], error) { return r.Get("about") }

This file is automatically generated and should not be manually edited.

Configuration

reg, err := templator.NewRegistry[HomeData]( fs, // Custom template directory templator.WithTemplatesPathHomeData, // Enable field validation templator.WithFieldValidation(HomeData{}), )

Development Requirements

License

MIT