A type-safe HTML template rendering engine for Go.
Note: This is an experimental project in active development. While it's stable enough for use, expect possible API changes. Feedback and contributions are welcome!
- Problem Statement
- Features
- Installation
- Quick Start
- Usage Examples
- Template Generation
- Configuration
- Contributing
- License
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.NewRegistry[HomeData](fs)
// 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
}{})
- Type-safe template execution with generics
- Concurrent-safe template management
- Custom template functions support
- Clean and simple API
- HTML escaping by default
go install github.com/alesr/templator
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)
}
}
// 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.NewRegistry[HomeData](fs)
aboutReg := templator.NewRegistry[AboutData](fs)
// 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
// Define your custom functions
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
}
// Create registry with functions
reg, err := templator.NewRegistry[PageData](fs,
templator.WithTemplateFuncs[PageData](funcMap))
// Use functions in your templates:
// <h1>{{.Title | upper}}</h1>
// Embedded FS
//go:embed templates/*
var embedFS embed.FS
reg := templator.NewRegistry[HomeData](embedFS)
// OS File System
reg := templator.NewRegistry[HomeData](os.DirFS("./templates"))
// In-Memory (testing)
fsys := fstest.MapFS{
"templates/home.html": &fstest.MapFile{
Data: []byte(`<h1>{{.Title}}</h1>`),
},
}
reg := templator.NewRegistry[HomeData](fsys)
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:
// <h1>{{.Title}}</h1> // ✅ OK - Title exists in ArticleData
// <p>{{.Content}}</p> // ✅ OK - Content exists in ArticleData
// invalid.html:
// <h1>{{.Author}}</h1> // ❌ Error - Author field doesn't exist
// <p>{{.PublishedAt}}</p> // ❌ 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.
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.
reg, err := templator.NewRegistry[HomeData](
fs,
// Custom template directory
templator.WithTemplatesPath[HomeData]("views"),
// Enable field validation
templator.WithFieldValidation(HomeData{}),
)
- Go 1.21 or higher
MIT