Skip to content

panyam/templar

Repository files navigation

Templar: Go Template Loader

Go Reference License: MIT

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.

Why Templar?

Templar is designed to integrate smoothly with Go's standard templating libraries while solving common issues:

  1. Minimal Learning Curve: If you know Go templates, you already know 99% of Templar.
  2. Zero New Runtime Syntax: The include directives are processed before rendering (variable based inclusion in the works).
  3. Flexible and Extensible: Create custom loaders for any template source (file loader for now, more in the works).
  4. Production Ready: Handles complex dependencies, prevents cycles, and provides clear error messages (and aiming to get better at this).

Background

Go's built-in templating libraries (text/template and html/template) are powerful but have limitations when working with complex template structures:

  1. No Native Dependency Management: When templates reference other templates, you must manually ensure they're loaded in the correct order.

  2. Global Template Namespace: All template definitions share a global namespace, making it challenging to use different versions of the same template in different contexts.

  3. Brittle Template Resolution: In large applications, templates often load differently in development vs. production environments.

  4. 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)
}

Proposal

Templar solves these problems by providing:

  1. 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>
  1. 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)
}
  1. Flexible Template Resolution: Multiple loaders can be configured to find templates in different locations.

  2. Template Reuse: The same template name can be reused in different contexts without conflict.

Getting Started

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)
  }
}

Key Features

1. Template Dependencies

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 }}

2. Template Namespacing

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 button are prefixed with the current namespace → NS:button
  • Names with : like Other:button are kept as-is (cross-namespace reference)
  • Names starting with :: like ::global become 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.

3. Template Extension (Inheritance)

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.

4. Multiple Template Loaders

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/")

5. Template Groups

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")
    },
})

6. External Template Sources (Vendoring)

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_modules

Reference 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 file

See vendoring.md for deployment strategies, configuration reference, and examples.

Advanced Usage

Conditional Template Loading

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))

Dynamic Templates

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)

Command Line Interface

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.html

Key commands:

  • templar init - Create a templar.yaml configuration file
  • templar get - Fetch external template sources for vendoring
  • templar sources - List configured sources and their status
  • templar serve - Start HTTP server to serve and test templates
  • templar debug - Analyze dependencies, detect cycles, visualize with GraphViz
  • templar 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.

Comparison with Other Solutions

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 (***) ⚠️ Partial

*: 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.

Other alternatives

  • 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).

Documentation

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

A GO template loader with dependency management, simplicity and extensibility in mind.

Resources

License

Stars

Watchers

Forks

Packages

No packages published