Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stencil

A minimal Go template engine built on html/template. Layout wrapping, reusable partials, and per-render overrides.

Features

  • Layout wrapping: Pages render inside a layout via {{ yield }}. Override or skip the layout per call.
  • Partials: Render reusable partials with {{ partial "name" . }}. Parsed once and cached for the lifetime of the engine.
  • fs.FS-based: Pass os.DirFS for development (reads from disk) or an embedded FS for production (templates in the binary, no disk I/O).
  • Zero dependencies: Only the Go standard library.
  • Safe for concurrent use: After construction the engine is read-only except for the partial cache, which uses sync.Map.

Installation

go get lowbit.dev/stencil

Usage

import (
    "io/fs"
    "net/http"
    "lowbit.dev/stencil"

    _ "embed"
)

//go:embed views layouts partials
var templateFS embed.FS

func main() {
    views, _    := fs.Sub(templateFS, "views")
    layouts, _  := fs.Sub(templateFS, "layouts")
    partials, _ := fs.Sub(templateFS, "partials")

    engine := stencil.New(stencil.Config{
        Views:         views,
        Layouts:       layouts,
        Partials:      partials,
        DefaultLayout: "base",
    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if err := engine.Render(w, "home", data); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    })
}

For development, skip the embed and pass os.DirFS directly — templates are re-read from disk on every render:

engine := stencil.New(stencil.Config{
    Views:         os.DirFS("views"),
    Layouts:       os.DirFS("layouts"),
    Partials:      os.DirFS("partials"),
    DefaultLayout: "base",
})

Config

Field Type Description
Views fs.FS Filesystem rooted at the page templates directory
Layouts fs.FS Filesystem rooted at the layouts directory
Partials fs.FS Filesystem rooted at the partials directory
DefaultLayout string Layout name used when no option is passed (without extension)
Extension string Template file extension, defaults to .html
Funcs template.FuncMap Custom functions available in all templates
DisableCache bool Re-parse partials on every render; useful during development with os.DirFS

Render options

// Use a different layout for this call
engine.Render(w, "about", data, stencil.WithLayout("minimal"))

// Render the page without any layout — useful for HTMX partials
engine.Render(w, "table-rows", data, stencil.WithoutLayout())

Template functions

yield

Renders the current page inside a layout. Call it once in your layout template:

<!-- layouts/base.html -->
<html>
  <body>{{ yield }}</body>
</html>

partial

Renders a partial by name, optionally passing data:

{{ partial "nav" . }}
{{ partial "alert" "Something went wrong" }}

Partial templates live in the Partials FS. They are parsed on first use and cached — the parse cost is paid once per partial per process lifetime.

Cache

Partial templates are cached in memory after the first render. For production (embed.FS) this cache is effectively permanent.

For development with os.DirFS you have two options:

  • Set DisableCache: true in the config. Partials are re-parsed from disk on every render — no manual invalidation needed.
  • Leave the cache enabled and call Reload() after changing a partial file to pick up the new version without restarting.
// Option 1 — always fresh, no manual step
engine := stencil.New(stencil.Config{
    ...
    DisableCache: true,
})

// Option 2 — cache on, invalidate manually
engine.Reload()

License

MIT

About

A minimal Go template engine built on `html/template`. Layout wrapping, reusable partials, and per-render overrides.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages