Template Sets
January 15, 2026 · View on GitHub
Template sets allow you to group templates with shared configuration, globals, and loaders. This is useful for separating different types of templates (e.g., web templates vs. email templates).
Understanding Template Sets
A TemplateSet is a container that holds:
- Template loaders - How templates are located and read
- Global variables - Variables available to all templates in the set
- Tag registry - Available tags (built-in + custom)
- Filter registry - Available filters (built-in + custom)
- Banned tags/filters - Sandbox restrictions
- Configuration - Debug mode, autoescape, whitespace options
- Template cache - Compiled templates for performance
Each template set is fully isolated from others. Custom tags, filters, globals, and sandbox restrictions in one set do not affect other sets.
DefaultSet and Package-Level Functions
pongo2 provides a DefaultSet for convenience. The package-level functions are shortcuts that operate on this default set:
// These package-level functions all use DefaultSet internally:
pongo2.FromFile("template.html") // → DefaultSet.FromFile()
pongo2.FromString("Hello {{ name }}!") // → DefaultSet.FromString()
pongo2.FromBytes([]byte("...")) // → DefaultSet.FromBytes()
pongo2.FromCache("template.html") // → DefaultSet.FromCache()
pongo2.RenderTemplateString(s, ctx) // → DefaultSet.RenderTemplateString()
pongo2.RenderTemplateFile(fn, ctx) // → DefaultSet.RenderTemplateFile()
// These also operate on DefaultSet:
pongo2.RegisterFilter("name", fn) // → DefaultSet.RegisterFilter()
pongo2.ReplaceFilter("name", fn) // → DefaultSet.ReplaceFilter()
pongo2.RegisterTag("name", parser) // → DefaultSet.RegisterTag()
pongo2.ReplaceTag("name", parser) // → DefaultSet.ReplaceTag()
pongo2.SetAutoescape(false) // → DefaultSet.SetAutoescape()
// Global variables for DefaultSet
pongo2.Globals["site_name"] = "My Website"
DefaultSet Configuration
The DefaultSet is initialized with:
DefaultLoader- ALocalFilesystemLoaderwith an empty base directory (uses current working directory)- Autoescape enabled - HTML escaping is on by default
- All built-in tags and filters - No restrictions
// This is essentially how DefaultSet is created internally:
DefaultLoader = MustNewLocalFileSystemLoader("")
DefaultSet = NewSet("default", DefaultLoader)
Security Consideration: DefaultSet Uses Local Filesystem
Important: The
DefaultSetuses aLocalFilesystemLoaderthat has unrestricted access to the local filesystem. For production applications, especially those handling user-generated content or requiring sandboxing, consider creating a custom template set with appropriate restrictions.
// For better security, create your own template set:
loader := pongo2.NewFSLoader(embeddedFS) // Use embedded templates
set := pongo2.NewSet("secure", loader)
set.BanTag("include") // Restrict file inclusion
set.BanTag("ssi")
set.BanFilter("safe") // Prevent autoescape bypass
See Security and Sandboxing for comprehensive security guidance.
Creating Custom Template Sets
// Create a new set with a loader
loader := pongo2.MustNewLocalFileSystemLoader("/path/to/templates")
mySet := pongo2.NewSet("email-templates", loader)
// Configure the set
mySet.Debug = true
mySet.Globals["company_name"] = "ACME Corp"
// Load templates from this set
tpl, err := mySet.FromFile("welcome.html")
Template Loaders
Loaders define how templates are located and read.
LocalFilesystemLoader
Loads templates from the local filesystem:
// Without base directory (uses working directory)
loader, err := pongo2.NewLocalFileSystemLoader("")
// With base directory
loader, err := pongo2.NewLocalFileSystemLoader("/var/templates")
// Panics on error (useful for init)
loader := pongo2.MustNewLocalFileSystemLoader("/var/templates")
With a base directory, all template paths are resolved relative to it:
loader := pongo2.MustNewLocalFileSystemLoader("/var/templates")
set := pongo2.NewSet("web", loader)
// Loads /var/templates/pages/home.html
tpl, err := set.FromFile("pages/home.html")
FSLoader
Supports Go's fs.FS interface (Go 1.16+):
import "embed"
//go:embed templates/*
var templateFS embed.FS
func main() {
loader := pongo2.NewFSLoader(templateFS)
set := pongo2.NewSet("embedded", loader)
tpl, err := set.FromFile("templates/page.html")
}
Works with any fs.FS implementation:
// os.DirFS
loader := pongo2.NewFSLoader(os.DirFS("/var/templates"))
// embed.FS
//go:embed templates
var templates embed.FS
loader := pongo2.NewFSLoader(templates)
Multiple Loaders
A template set can have multiple loaders. Templates are resolved in order:
loader1 := pongo2.MustNewLocalFileSystemLoader("/var/templates/custom")
loader2 := pongo2.MustNewLocalFileSystemLoader("/var/templates/default")
set := pongo2.NewSet("multi", loader1, loader2)
set.AddLoader(anotherLoader) // Add more later
// Tries loader1 first, then loader2
tpl, err := set.FromFile("page.html")
Custom Loaders
Implement the TemplateLoader interface:
type TemplateLoader interface {
// Abs calculates the absolute path to a template
// base is the parent template's path (for includes/extends)
// name is the requested template name
Abs(base, name string) string
// Get returns a reader for the template content
Get(path string) (io.Reader, error)
}
Example database loader:
type DBLoader struct {
db *sql.DB
}
func (l *DBLoader) Abs(base, name string) string {
return name // Templates are identified by name only
}
func (l *DBLoader) Get(path string) (io.Reader, error) {
var content string
err := l.db.QueryRow("SELECT content FROM templates WHERE name = ?", path).Scan(&content)
if err != nil {
return nil, err
}
return strings.NewReader(content), nil
}
Template Set Options
Debug Mode
When enabled:
- Templates are recompiled on every request (no caching)
ExecutionContext.Logf()outputs to STDOUT
set.Debug = true
TrimBlocks and LStripBlocks
Control whitespace around block tags:
set.Options.TrimBlocks = true // Remove first newline after block tags
set.Options.LStripBlocks = true // Strip leading whitespace before block tags
Example:
{% if true %}
Hello
{% endif %}
Without options: \nHello\n
With TrimBlocks: Hello\n
With LStripBlocks: \nHello\n
With both: Hello\n
Global Variables
Variables available to all templates in a set:
set.Globals["site_name"] = "My Website"
set.Globals["current_year"] = time.Now().Year()
set.Globals["version"] = "1.0.0"
// Functions are also allowed
set.Globals["format_price"] = func(price float64) string {
return fmt.Sprintf("$%.2f", price)
}
Access in templates:
<footer>© {{ current_year }} {{ site_name }}</footer>
{{ format_price(19.99) }}
Template Caching
FromCache
Caches compiled templates for production use:
// First call compiles and caches
tpl, err := set.FromCache("page.html")
// Subsequent calls return cached template
tpl, err := set.FromCache("page.html")
When Debug is true, caching is disabled.
CleanCache
Clear the template cache:
// Clear specific templates
set.CleanCache("page.html", "header.html")
// Clear entire cache
set.CleanCache()
Autoescape
Control automatic HTML escaping:
// Configure autoescape for a template set
set.SetAutoescape(false) // Disable HTML escaping
set.SetAutoescape(true) // Enable HTML escaping (default)
// Or use the global function for DefaultSet
pongo2.SetAutoescape(false)
Per-Set Tags and Filters
Each template set has its own tag and filter registries. This allows different template sets to have different custom extensions.
Registering Set-Specific Filters
// Register a filter only for this set
err := set.RegisterFilter("custom_filter", myFilterFunc)
// Replace an existing filter in this set only
err := set.ReplaceFilter("upper", myUpperFilter)
// Check if a filter exists in this set
if set.FilterExists("custom_filter") {
// Filter is available
}
// Apply a filter programmatically using this set's registry
result, err := set.ApplyFilter("upper", pongo2.AsValue("hello"), nil)
// Panic version
result := set.MustApplyFilter("upper", pongo2.AsValue("hello"), nil)
Registering Set-Specific Tags
// Register a tag only for this set
err := set.RegisterTag("custom_tag", myTagParser)
// Replace an existing tag in this set only
err := set.ReplaceTag("for", myForParser)
// Check if a tag exists in this set
if set.TagExists("custom_tag") {
// Tag is available
}
Isolation Example
// Create two isolated template sets
webSet := pongo2.NewSet("web", webLoader)
emailSet := pongo2.NewSet("email", emailLoader)
// Register a filter only for web templates
webSet.RegisterFilter("asset_url", assetUrlFilter)
// Register a tag only for email templates
emailSet.RegisterTag("unsubscribe", unsubscribeTagParser)
// asset_url filter is NOT available in emailSet
// unsubscribe tag is NOT available in webSet
Sandbox Features
Restrict what templates can do by banning specific tags and filters. For comprehensive security guidance, see Security and Sandboxing.
Banning Tags
set := pongo2.NewSet("restricted", loader)
// Ban dangerous tags before loading any templates
set.BanTag("include") // Disable file inclusion
set.BanTag("import") // Disable macro imports
set.BanTag("ssi") // Disable server-side includes
// Now load templates
tpl, err := set.FromFile("user-content.html")
Banning Filters
set.BanFilter("safe") // Prevent bypassing autoescape
Restrictions
- Tags and filters must be banned BEFORE the first template is loaded
- Once a template is loaded, the set is "locked"
- Attempting to ban after loading returns an error
set := pongo2.NewSet("test", loader)
// This works
set.BanTag("ssi")
// Load a template
tpl, _ := set.FromFile("page.html")
// This returns an error - too late!
err := set.BanTag("include")
// err: "you cannot ban any tags after you've added your first template"
Built-in pongo2 Context
Templates have access to pongo2 metadata via pongo2:
pongo2 version: {{ pongo2.version }}
Complete Example
package main
import (
"log"
"net/http"
"time"
"github.com/flosch/pongo2/v7"
)
var templates *pongo2.TemplateSet
func init() {
// Create loader
loader := pongo2.MustNewLocalFileSystemLoader("./templates")
// Create template set
templates = pongo2.NewSet("web", loader)
// Configure options
templates.Options.TrimBlocks = true
templates.Options.LStripBlocks = true
// Set global variables
templates.Globals["site_name"] = "My App"
templates.Globals["current_year"] = time.Now().Year()
// Helper functions
templates.Globals["asset_url"] = func(path string) string {
return "/static/" + path
}
// Sandbox restrictions
templates.BanTag("ssi")
templates.BanTag("include") // Force explicit template paths
}
func handler(w http.ResponseWriter, r *http.Request) {
tpl, err := templates.FromCache("pages/home.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = tpl.ExecuteWriter(pongo2.Context{
"user": getCurrentUser(r),
"items": getItems(),
}, w)
if err != nil {
log.Printf("Template error: %v", err)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}