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).
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.
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"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)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 bypassSee Security and Sandboxing for comprehensive security guidance.
// 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")Loaders define how templates are located and read.
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")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)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")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
}When enabled:
- Templates are recompiled on every request (no caching)
ExecutionContext.Logf()outputs to STDOUT
set.Debug = trueControl whitespace around block tags:
set.Options.TrimBlocks = true // Remove first newline after block tags
set.Options.LStripBlocks = true // Strip leading whitespace before block tagsExample:
{% if true %}
Hello
{% endif %}Without options: \nHello\n
With TrimBlocks: Hello\n
With LStripBlocks: \nHello\n
With both: Hello\n
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) }}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.
Clear the template cache:
// Clear specific templates
set.CleanCache("page.html", "header.html")
// Clear entire cache
set.CleanCache()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)Each template set has its own tag and filter registries. This allows different template sets to have different custom extensions.
// 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)// 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
}// 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 webSetRestrict what templates can do by banning specific tags and filters. For comprehensive security guidance, see Security and Sandboxing.
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")set.BanFilter("safe") // Prevent bypassing autoescape- 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"Templates have access to pongo2 metadata via pongo2:
pongo2 version: {{ pongo2.version }}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)
}