Skip to content

Filesystems

A registry holds an fs.FS, and every template lookup is a read against that filesystem. This single design choice drives all the conveniences below — and it means templator never touches your host filesystem directly.

Get("components/header") reads <templates-path>/components/header.html inside the registry’s fs.FS. The default path is templates; override it with WithTemplatesPath:

reg, _ := templator.NewRegistry[PageData](
fsys,
templator.WithTemplatesPath[PageData]("views"),
)

Keep in mind that embed.FS roots are the package directory, and os.DirFS(dir) roots are dir — the same templates/home.html file is addressed as "templates/home.html" from one and "home.html" from the other. templator always prepends the templates path, and an empty WithTemplatesPath is ignored, so there is no root-relative mode: whatever fs.FS you hand the registry must contain a directory literally named templates — or the value of WithTemplatesPath — directly under its root. If your templates live at web/templates/ inside the filesystem you have, bracket the parent, not the templates directory:

web/templates/home.html
sub, _ := fs.Sub(fsys, "web") // parent of the templates dir
reg, _ := templator.NewRegistry[HomeData](sub)
home, _ := reg.Get("home") // resolves to web/templates/home.html
// fs.Sub(fsys, "web/templates") would NOT work here: the registry
// would look for web/templates/templates/home.html and fail with an fs error.

The three filesystems you will actually use

Section titled “The three filesystems you will actually use”

The zero-config setup from the Quickstart. Templates are separate files you can edit and reload by rebuilding the registry:

fs := os.DirFS(".")
reg, _ := templator.NewRegistry[HomeData](fs)

Good for servers deployed alongside their templates and for local development.

Templates shipped inside a single binary. Deploy one file; nothing to forget:

import "embed"
//go:embed templates
var templateFS embed.FS
reg, _ := templator.NewRegistry[HomeData](templateFS)

Embedding the directory (as above) includes its files recursively. The embed FS is rooted at the declaring package, so the file lands at templates/home.html — which is exactly the registry’s default path, so no option is needed. You only need WithTemplatesPath when the embedded root is not called templates, for example:

//go:embed html
var templateFS embed.FS
reg, _ := templator.NewRegistry[HomeData](
templateFS,
templator.WithTemplatesPath[HomeData]("html"),
)

The test harness. Templates are declared as data, so tests define exactly what each case needs — no fixture directories, no I/O:

fsys := fstest.MapFS{
"templates/home.html": &fstest.MapFile{
Data: []byte(`<h1>{{.Title}}</h1>`),
},
}
reg, _ := templator.NewRegistry[HomeData](fsys)

Full worked tests are in Test templates.

  • Testability without fixtures. Site templates, malformed templates, and nested-layout cases are interchangeable data, not files on disk.
  • Test data fidelity. fstest.MapFS errors match what real filesystems return, so your error-path tests behave like production.
  • No hidden state. The registry reads nothing outside the filesystem you hand it. It keeps no goroutines, watchers, or caches beyond the parsed handlers, so lifecycles are simple: keep the registry for the life of the app, drop it to force a re-parse.
  • Modularity. Any fs.FS-shaped thing works: a sub-FS (fs.Sub), a read-only overlay, or your own wrapper.