Templar is a powerful extension to Go's standard templating libraries that adds dependency management, simplifies template composition, and solves common pain points in template organization.
Templar is designed to integrate smoothly with Go's standard templating libraries while solving common issues:
- Minimal Learning Curve: If you know Go templates, you already know 99% of Templar.
- Zero New Runtime Syntax: The include directives are processed before rendering (variable based inclusion in the works).
- Flexible and Extensible: Create custom loaders for any template source (file loader for now, more in the works).
- Production Ready: Handles complex dependencies, prevents cycles, and provides clear error messages (and aiming to get better at this).
Go's built-in templating libraries (text/template and html/template) are powerful but have limitations when working with complex template structures:
-
No Native Dependency Management: When templates reference other templates, you must manually ensure they're loaded in the correct order.
-
Global Template Namespace: All template definitions share a global namespace, making it challenging to use different versions of the same template in different contexts.
-
Brittle Template Resolution: In large applications, templates often load differently in development vs. production environments.
-
Verbose Template Loading: Loading templates with their dependencies typically requires repetitive boilerplate code:
// Standard approach - verbose and error-prone
func renderIndexPage(w http.ResponseWriter, r *http.Request) {
t := template.ParseFiles("Base1.tmpl", "a2.tmpl", "IndexPage.tmpl")
t.Execute(w, data)
}
func renderProductListPage(w http.ResponseWriter, r *http.Request) {
t := template.ParseFiles("AnotherBase.tmpl", "a2.tmpl", "ProductListPage.tmpl")
t.Execute(w, data)
}Templar solves these problems by providing:
- Dependency Declaration: Templates can declare their own dependencies using a simple include syntax:
{{# include "base.tmpl" #}}
{{# include "components/header.tmpl" #}}
<div class="content">
{{ template "content" . }}
</div>
- Automatic Template Loading: Templar automatically loads and processes all dependencies:
// With Templar - clean and maintainable
func renderIndexPage(w http.ResponseWriter, r *http.Request) {
tmpl := loadTemplate("IndexPage.tmpl") // Dependencies automatically handled
tmpl.Execute(w, data)
}-
Flexible Template Resolution: Multiple loaders can be configured to find templates in different locations.
-
Template Reuse: The same template name can be reused in different contexts without conflict.
package main
import (
"os"
"github.com/panyam/templar"
)
func main() {
// Create a template group
group := templar.NewTemplateGroup()
// Create a filesystem loader that searches multiple directories
group.Loader = templar.NewFileSystemLoader(
"templates/",
"templates/shared/",
)
// Load a root template (dependencies handled automatically)
rootTemplate := group.MustLoad("pages/homepage.tmpl", "")
// Prepare data for the template
data := map[string]any{
"Title": "Home Page",
"User": User{
ID: 1,
Name: "John Doe",
},
"Updates": []Update{
{Title: "New Feature Released", Date: "2023-06-15"},
{Title: "System Maintenance", Date: "2023-06-10"},
{Title: "Welcome to our New Site", Date: "2023-06-01"},
},
"Featured": FeaturedContent{
Title: "Summer Sale",
Description: "Get 20% off on all products until July 31st!",
URL: "/summer-sale",
},
}
// Render the template to stdout (for this example)
if err = group.RenderHtmlTemplate(os.Stdout, rootTemplate[0], "", data, nil); err != nil {
fmt.Printf("Error rendering template: %v\n", err)
}
}In your templates, use the {{# include "path/to/template" #}} directive to include dependencies:
{{# include "layouts/base.tmpl" #}}
{{# include "components/navbar.tmpl" #}}
{{ define "content" }}
<h1>Welcome to our site</h1>
<p>This is the homepage content.</p>
{{ end }}You can also selectively include only specific templates (tree-shaking):
{{# include "forms.tmpl" "button" "input" #}}
{{ define "page" }}
{{ template "button" . }} {{/* Only button and input are included */}}
{{ end }}Avoid template name collisions by importing templates into namespaces:
{{# namespace "UI" "widgets/buttons.tmpl" #}}
{{# namespace "Theme" "themes/bootstrap.tmpl" #}}
{{ define "page" }}
{{ template "UI:button" dict "Text" "Click Me" }}
{{ template "Theme:header" . }}
{{ end }}Namespace resolution rules:
- Plain names like
buttonare prefixed with the current namespace →NS:button - Names with
:likeOther:buttonare kept as-is (cross-namespace reference) - Names starting with
::like::globalbecome global (no namespace)
Tree-shaking is also supported with namespaces:
{{# namespace "UI" "widgets.tmpl" "button" "icon" #}}
{{/* Only button, icon, and their dependencies are included */}}See namespace.md for detailed examples, the diamond problem, and common gotchas.
Extend base templates while overriding specific blocks:
{{# namespace "Base" "layouts/base.tmpl" #}}
{{# extend "Base:layout" "MyLayout" "Base:title" "myTitle" "Base:content" "myContent" #}}
{{ define "myTitle" }}Custom Page Title{{ end }}
{{ define "myContent" }}
<h1>My Custom Content</h1>
{{ end }}
{{ template "MyLayout" . }}This creates a new template MyLayout by copying Base:layout, but rewiring:
{{ template "Base:title" . }}→{{ template "myTitle" . }}{{ template "Base:content" . }}→{{ template "myContent" . }}
Non-overridden blocks retain their original references to the base templates.
Important: The extend directive only rewrites template calls within the copied template itself, not in templates it calls. For nested overrides, you need to extend each level of the hierarchy. See extend.md for detailed examples, visual diagrams, and common gotchas.
Templar allows you to configure multiple template loaders with fallback behavior:
// Create a list of loaders to search in order
loaderList := &templar.LoaderList{}
// Add loaders in priority order
loaderList.AddLoader(templar.NewFileSystemLoader("app/templates/"))
loaderList.AddLoader(templar.NewFileSystemLoader("shared/templates/"))
// Set a default loader as final fallback
loaderList.DefaultLoader = templar.NewFileSystemLoader("default/templates/")Template groups manage collections of templates and their dependencies:
group := templar.NewTemplateGroup()
group.Loader = loaderList
group.AddFuncs(map[string]any{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
})Load templates from external sources like GitHub repositories:
# templar.yaml
sources:
goapplib:
url: github.com/panyam/goapplib
path: templates
ref: v1.2.0
vendor_dir: ./templar_modules
search_paths:
- ./templates
- ./templar_modulesReference external templates with the @sourcename prefix:
{{# namespace "EL" "@goapplib/components/EntityListing.html" #}}
{{ define "MyPage" }}
{{ template "EL:EntityListing" .Items }}
{{ end }}Fetch dependencies with:
templar get # Fetch all sources
templar get --update # Update to latest versions
templar get --verify # Verify local matches lock fileSee vendoring.md for deployment strategies, configuration reference, and examples.
You can implement conditional template loading based on application state:
folder := "desktop"
if isMobile {
folder = "mobile"
}
tmpl, err := loader.Load(fmt.Sprintf("%s/homepage.tmpl", folder))Generate templates dynamically and use them immediately:
dynamicTemplate := &templar.Template{
Name: "dynamic-template",
RawSource: []byte(`Hello, {{.Name}}!`),
}
group.RenderTextTemplate(w, dynamicTemplate, "", map[string]any{"Name": "World"}, nil)Templar provides a CLI tool for serving templates, debugging dependencies, and managing external sources:
# Install
go install github.com/panyam/templar/cmd/templar@latest
# Initialize a new project
templar init
# Fetch external template sources
templar get
# List configured sources
templar sources
# Start development server
templar serve -t ./templates -s /static:./public
# Debug template dependencies
templar debug -p templates homepage.htmlKey commands:
templar init- Create a templar.yaml configuration filetemplar get- Fetch external template sources for vendoringtemplar sources- List configured sources and their statustemplar serve- Start HTTP server to serve and test templatestemplar debug- Analyze dependencies, detect cycles, visualize with GraphViztemplar version- Print version information
Configuration via .templar.yaml or environment variables (TEMPLAR_ prefix).
See cli.md for complete command reference, flags, configuration options, and examples.
| Feature | Standard Go Templates | Templar |
|---|---|---|
| Dependency Management | ❌ | ✅ |
| Self-describing Templates (*) | ❌ | ✅ |
| Template Namespacing | ❌ | ✅ |
| Template Extension/Inheritance | ❌ | ✅ |
| Tree-shaking | ❌ | ✅ |
| Standard Go Template Syntax | ✅ | ✅ |
| Supports Cycles Prevention (**) | ❌ | ✅ |
| HTML Escaping | ✅ | ✅ |
| Template Grouping (***) | ✅ |
*: Self-describing here refers to a template specifying all the dependencies it needs so a template author can be
clear about what is required and include them instead of hoping they exist somehow.
**: Cycles are caught by the preprocessor and is clearer.
***: Grouping in standard templates is done in code by the template user instead of the author.
- Pongo2 is amazing for its reverence for Django syntax.
- Templ is amazing as a typed template library and being able to perform compile time validations of templates.
My primary goal here was to have as much alignment with Go's template stdlib. Beyond this library for managing dependencies, the goal itself was to have strict adherence to Go's templating syntax. Using the same Go template syntax also allows extra features during preprocessing of templates. (eg using same set of variables for both pre-processing as well as for final rendering).
- Motivation - Why templar was created
- Namespacing - Avoiding name collisions with namespace imports
- Template Extension - Inheriting and overriding templates
- Vendoring - Loading templates from external sources (GitHub, etc.)
- CLI Reference - Command line tool usage, flags, and configuration
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.