Skip to content

template system based on the standard golang templates package (layouts, pages, partials, headless_cms support, ...)

License

Notifications You must be signed in to change notification settings

dryaf/templates

Repository files navigation

templates

License Coverage Go Report Card GoDoc

A secure, file-system-based Go template engine built on Google's safehtml/template. It provides a familiar structure of layouts, pages, and reusable blocks (partials) while ensuring output is safe from XSS vulnerabilities by default.

Why this library?

Go's standard html/template is good, but Google's safehtml/template is better, providing superior, context-aware automatic escaping that offers stronger security guarantees against XSS. However, safehtml/template can be complex to set up, especially for projects using a traditional layout/page/partial structure.

This library provides a simple, opinionated framework around safehtml/template so you can get the security benefits without the setup overhead.

Features

  • Secure by Default: Built on safehtml/template to provide contextual, automatic output escaping.
  • Optional Security Relaxing: Disable safehtml for rapid prototyping or projects using HTMX when the strict type system becomes a hindrance.
  • Layouts, Pages, and Blocks: Organizes templates into a familiar and powerful structure. Render pages within different layouts, or render blocks individually.
  • Live Reloading: Automatically re-parses templates on every request for a seamless development experience.
  • Production-Ready: Uses Go's embed.FS to compile all templates and assets into a single binary for production deployments.
  • Dynamic Rendering: Includes a d_block helper to render blocks dynamically by name—perfect for headless CMS integrations.
  • Convenient Helpers: Comes with a locals function to easily pass key-value data to blocks.
  • Framework Integrations: Provides optional, lightweight integration packages for net/http, Echo, chi, and gin-gonic/gin.

Installation

Add the library to your go.mod file:

go get github.com/dryaf/templates

Then import it in your code:

import "github.com/dryaf/templates"

Quick Start

  1. Create your template files:

    .
    └── files
        └── templates
            ├── layouts
            │   └── application.gohtml
            └── pages
                └── home.gohtml
    

    files/templates/layouts/application.gohtml:

    {{define "layout"}}
    <!DOCTYPE html>
    <html><body>
        <h1>Layout</h1>
        {{block "page" .}}{{end}}
    </body></html>
    {{end}}

    files/templates/pages/home.gohtml:

    {{define "page"}}
    <h2>Home Page</h2>
    <p>Hello, {{.}}!</p>
    {{end}}
  2. Write your Go application:

    package main
    
    import (
    	"log"
    	"net/http"
    
    	"github.com/dryaf/templates"
    	"github.com/dryaf/templates/integrations/stdlib"
    )
    
    func main() {
    	// For development, New() uses the local file system.
    	// For production, you would pass in an embed.FS.
    	tmpls := templates.New()
    	tmpls.AlwaysReloadAndParseTemplates = true // Recommended for development
    	tmpls.MustParseTemplates()
    
    	renderer := stdlib.FromTemplates(tmpls)
    
    	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    		err := renderer.Render(w, r, http.StatusOK, "home", "World")
    		if err != nil {
    			log.Println(err)
    			http.Error(w, "Internal Server Error", 500)
    		}
    	})
    
    	log.Println("Starting server on :8080")
    	http.ListenAndServe(":8080", nil)
    }

Configuration

The New function accepts Functional Options for flexible configuration:

  • WithFileSystem(fs fs.FS): Sets the filesystem (OS or Embed).
  • WithRoot(path string): Sets the root directory for templates.
  • WithFuncMap(fm template.FuncMap): Adds custom helper functions.
  • WithReload(enabled bool): Enables auto-reloading for development.
  • WithDisableSafeHTML(disabled bool): Disables the safehtml engine and falls back to standard text/template.
  • WithDisableTrustedLog(disabled bool): Disables INFO logs when using trusted_*_ctx helpers.
  • WithLogger(logger *slog.Logger): Sets a custom logger.

Rapid Prototyping Mode

If you find safehtml's strict type system too restrictive during early development, or for projects that don't require high security (like internal tools using HTMX), you can disable it. The engine will use text/template, and the trusted_* functions will act as literal passthroughs for your raw data.

tmpls := templates.New(
    templates.WithDisableSafeHTML(true),
    templates.WithDisableTrustedLog(true), // Suppress info logs for bypasses
)

Dev Mode (OS Filesystem)

// Defaults to OS filesystem, rooted at "files/templates"
tmpls := templates.New(
    templates.WithReload(true), // Enable hot-reloading
)

Production Mode (Embedded Filesystem)

//go:embed files/templates
var templatesFS embed.FS

// ...

tmpls := templates.New(
    templates.WithFileSystem(&templatesFS),
    templates.WithRoot("files/templates"),
)

Error Handling

The library returns structured errors that can be checked using errors.Is():

  • templates.ErrTemplateNotFound: The requested template or layout was not found.
  • templates.ErrBlockNotFound: The requested block was not found.
  • templates.ErrInvalidBlockName: The block name format is invalid (must start with _).
err := tmpls.RenderBlockAsHTMLString("_non_existent", nil)
if errors.Is(err, templates.ErrBlockNotFound) {
    // Handle missing block
}

Core Concepts

Directory Structure

The engine expects a specific directory structure by default, located at ./files/templates:

  • files/templates/layouts/: Contains layout templates. Each file defines a "layout".
  • files/templates/pages/: Contains page templates.
  • files/templates/blocks/: Contains reusable blocks (partials).

Important Template Rules

  1. Pages must define "page": Every template file in the pages directory must define its main content within {{define "page"}}...{{end}}.
  2. Blocks must define "_name": Every template file in the blocks directory must define a template, and that definition's name must match the filename and be prefixed with an underscore. For example, _form.gohtml must contain {{define "_form"}}...{{end}}.

Rendering Syntax

You have fine-grained control over how templates are rendered:

  • "page_name": Renders the page within the default layout (application.gohtml).
  • "layout_name:page_name": Renders the page within a specific layout.
  • ":page_name": Renders the page without any layout.
  • "_block_name": Renders a specific block by itself.

Dynamic Blocks (d_block)

To render a block whose name is determined at runtime (e.g., from a database or CMS API), you can use d_block. This is a powerful feature for dynamic page composition.

<!-- Instead of this, which requires the block name to be static: -->
{{block "_header" .}}{{end}}

<!-- You can do this: -->
{{d_block .HeaderBlockName .HeaderBlockData}}

Passing locals to Blocks

Passing maps as context to blocks can be verbose. The locals helper function makes it easy to create a map on the fly. It accepts a sequence of key-value pairs.

<!-- Standard block call with locals -->
{{block "_user_card" (locals "Name" "Alice" "Age" 30)}}{{end}}

<!-- Dynamic block call with locals -->
{{locals "Name" "Bob" "Age" 42 | d_block "_user_card"}}

_user_card.gohtml:

{{define "_user_card"}}
<div class="card">
    <h3>{{.Name}}</h3>
    <p>Age: {{.Age}}</p>
</div>
{{end}}

Passing references to Blocks

The references helper is similar to locals, but it ensures that every value passed to the map is a pointer. This is useful when your template logic expects pointers (e.g. for nil checks or to avoid copying large structs).

If a value is already a pointer, it is used as is. If it is not a pointer, a new pointer to a copy of the value is created.

<!-- Passing values as references -->
{{block "_user_edit" (references "User" .User "IsActive" true)}}

Security and trusted_* Functions

This library uses safehtml/template, which provides protection against XSS by default. It contextually escapes variables.

Sometimes, you receive data from a trusted source (like a headless CMS) that you know is safe and should not be escaped. For these cases, you can use the trusted_* template functions, which wrap the input string in the appropriate safehtml type.

  • trusted_html: For HTML content.
  • trusted_script: For JavaScript code.
  • trusted_style: For CSS style declarations.
  • trusted_stylesheet: For a full CSS stylesheet.
  • trusted_url: For a general URL.
  • trusted_resource_url: For a URL that loads a resource like a script or stylesheet.
  • trusted_identifier: For an HTML ID or name attribute.

Example:

<!-- This will be escaped: -->
<p>{{.UnsafeHTMLFromUser}}</p>

<!-- This will be rendered verbatim, because you are vouching for its safety: -->
<div>
    {{trusted_html .SafeHTMLFromCMS}}
</div>

Note: If DisableSafeHTML is enabled, these functions will simply return your objects as-is, allowing them to bypass the standard auto-escaper.

Context-Aware Helpers

For structured logging and auditing, this library provides context-aware versions of d_block and trusted_* functions. These helpers require a context.Context as the first argument.

d_block_ctx

Same as d_block, but logs errors (e.g. missing blocks) using the Logger attached to Templates.

// In template
{{ d_block_ctx .Ctx "my_block_name" .Data }}

trusted_*_ctx

Context-aware versions of all trusted_* functions. They log an INFO message when called, which is useful for security auditing to track when and where bypasses are used.

  • trusted_html_ctx
  • trusted_script_ctx
  • trusted_style_ctx
  • trusted_stylesheet_ctx
  • trusted_url_ctx
  • trusted_resource_url_ctx
  • trusted_identifier_ctx

Usage:

// In template
{{ trusted_html_ctx .Ctx "<b>Bold Content</b>" }}

You can disable these logs using WithDisableTrustedLog(true).

Integrations

net/http (stdlib)

The integrations/stdlib package provides a simple renderer for use with net/http.

import "github.com/dryaf/templates/integrations/stdlib"

// --- inside main ---
// Create the renderer, configuring the underlying templates instance with functional options
renderer := stdlib.New(
    templates.WithReload(true), // Example option: enable hot-reloading
)

// Use it in an http.HandlerFunc
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    renderer.Render(w, r, http.StatusOK, "home", "Data")
})

// Or create a handler that always renders the same template
http.Handle("/about", renderer.Handler("about", nil))

Echo

The integrations/echo package provides a renderer for the Echo framework.

import "github.com/dryaf/templates/integrations/echo"
// ...
e := echo.New()
e.Renderer = templates_echo.Renderer(tmpls)

e.GET("/", func(c echo.Context) error {
    return c.Render(http.StatusOK, "home", "World")
})

Chi

The integrations/chi package provides a renderer compatible with the chi router.

import "github.com/dryaf/templates/integrations/chi"
// ...
renderer := chi.FromTemplates(tmpls)
r := chi.NewRouter()

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    renderer.Render(w, r, http.StatusOK, "home", "Chi")
})

Gin

The integrations/gin package provides a renderer that implements gin.HTMLRender for the Gin framework.

import "github.com/dryaf/templates/integrations/gin"
// ...
router := gin.Default()
router.HTMLRender = templates_gin.New(tmpls)

router.GET("/", func(c *gin.Context) {
    c.HTML(http.StatusOK, "home", "Gin")
})

Roadmap

The library is considered feature-complete and stable for a v1.0.0 release. Future development will be driven by community feedback and integration requests for new frameworks. Potential ideas include:

  • Additional template helper functions.
  • Performance optimizations.

Feel free to open an issue to suggest features or improvements.

License

This project is licensed under the MIT License.

About

template system based on the standard golang templates package (layouts, pages, partials, headless_cms support, ...)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •