Serve HTML
Two modules, split along one line: gas/template decides where template
source lives, gas/ui decides how it renders. That separation is what
lets you develop against files on disk and ship a single binary with templates
embedded, without the rendering code noticing.
go get github.com/gasmod/gas/template github.com/gasmod/gas/uiWiring
Section titled “Wiring”app := gas.NewApp( gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// Where templates come from. The fs store takes an fs.FS, so os.DirFS // during development and an embed.FS in a shipped binary. gas.WithSingletonService[gas.TemplateProvider]( templatefs.NewStore(os.DirFS("templates")), ),
// What renders them. The type parameter names the template provider to // inject; the registration key is the interface consumers ask for. gas.WithSingletonService[gas.UIProvider](ui.New[gas.TemplateProvider]()),)Where templates come from
Section titled “Where templates come from”| Backend | Use it for |
|---|---|
template/fs |
An fs.FS. os.DirFS in development, embed.FS in a release binary |
template/dir |
A directory on disk, with an in-memory overlay for runtime additions |
template/memory |
Tests and ephemeral content |
template/db |
Templates users can edit, across multiple instances |
template/composite |
Chain them: write to the first, read through all |
The composite store is the interesting one. Put a database store first and an embedded store behind it, and you get user-editable templates that fall back to the versions you shipped.
Directory convention
Section titled “Directory convention”Directorytemplates/
Directorylayouts/
- base.html the entry point,
{{define "base"}}
- base.html the entry point,
Directorypartials/
- nav.html parsed into every render
- footer.html
- home.html a page, rendered as
Render(w, "home", data) Directorynotes/
- list.html nested pages work,
"notes/list"
- list.html nested pages work,
Everything in layouts/ and partials/ is parsed into every render. Everything
else is a page. A page that defines blocks gets wrapped in the layout; a page
that defines none renders standalone.
Rendering
Section titled “Rendering”// A service takes gas.UIProvider and never imports gas/ui.type Service struct { ui gas.UIProvider}
func New(ui gas.UIProvider) *Service { return &Service{ui: ui} }
func (s *Service) home(w http.ResponseWriter, r *http.Request) { _ = s.ui.Render(w, "home", map[string]any{ "SiteName": "Gas", "Name": "Ahmed", })}Your service takes gas.UIProvider and never imports gas/ui, so it can be
tested against uitest.MockUI.
RenderFragment renders a page without the layout wrapper, which is exactly
what an HTMX swap wants. Same template, two entry points:
// HTMX asks for a fragment, a browser asks for a full page. Same template.func (s *Service) notes(w http.ResponseWriter, r *http.Request) { data := map[string]any{"Notes": []string{"first", "second"}}
if r.Header.Get("HX-Request") == "true" { _ = s.ui.RenderFragment(w, "notes/list", data) return } _ = s.ui.Render(w, "notes/list", data)}Template functions
Section titled “Template functions”Built in: safe, safeAttr, safeURL, upper, lower, title, trimSpace,
contains, hasPrefix, hasSuffix, replace, join, split, truncate,
now, formatTime, formatTimePtr, add, sub, dict, list, json, and
buildId.
dict is the one you will use most, since it is how a partial gets arbitrary
data: {{template "user-card" dict "Name" .UserName "Role" "admin"}}.
buildId returns a stable id per process, and a fresh one per call in
development, which makes it a good cache-busting query parameter on asset URLs.
Any service can contribute helpers during Init:
// Any service can contribute template helpers during Init. Templates are built// lazily on first render, so registrations made in Init are always in time.func (s *Service) Init() error { s.ui.RegisterFuncs(map[string]any{ "formatDate": func(layout string) string { return layout }, }) return nil}Templates are built lazily on first render, so registrations made during Init
are always in time. A name that collides with a built-in logs a warning.
Static files
Section titled “Static files”Set UI.StaticDir (or pass ui.WithStaticFS for an embed.FS) and the service
registers a route serving it, with directory listing blocked. Three settings,
three separate jobs:
| Setting | Answers |
|---|---|
StaticDir / WithStaticFS |
What to serve |
StaticPath / StaticPaths |
Where to serve it, as URL patterns |
StaticStripPrefix |
What to strip before looking up the file |
If the FS mirrors your URLs, strip nothing. If the FS is flat and the URLs are not, strip the prefix. When empty, nothing is stripped.
Development mode
Section titled “Development mode”When GasEnv is development-like, templates rebuild on every request, so edits
show up on refresh. In production they are built once.