Core concepts
Templates
Where templates live, how they are loaded and embedded, how they reload in development, slots, built-in and custom functions, and escaping.
Every fragment renders one template. Templates are Go's
html/template — the same syntax, the same
contextual escaping — with a handful of functions added for the things a
fragment needs: its slots, links to other pages, assets, forms and the page's
<head>.
<!-- templates/pages/post.html -->
<article>
<h1>{{.Title}}</h1>
<p class="meta">{{formatTime .Published "2 January 2006"}}</p>
{{slot "author"}}
<div class="body">{{.Body}}</div>
</article>
Where templates live
Config.Template says where to find them:
app, err := collage.New(&collage.Config{
Template: collage.TemplateConfig{
Root: "templates",
Extension: ".html",
},
})
Every file under Root whose extension is Extension is a template. Root
defaults to ./templates and Extension to .html; files with any other
extension are ignored, so a README.md beside the templates is harmless.
A template's name is its path relative to Root, extension included, and that is
what a fragment names:
templates/
├── layouts/default.html → "layouts/default.html"
├── pages/post.html → "pages/post.html"
└── fragments/author.html → "fragments/author.html"
collage.NewFragment("author", "fragments/author.html")
Loaded once, checked early
collage.New parses every template into one set, before anything is served.
Each kind of mistake is caught at the earliest point it can be:
- A root that does not exist fails
NewwithErrTemplateRootMissing— the most common startup failure there is, and worth telling apart from a template that does not parse. - A template that does not parse, or that calls a function nobody registered,
fails
Newwith the file named. - A fragment naming a template that was not loaded fails
RegisterPagewithErrTemplateNotFound, naming the page, the fragment and the path. - A template that fails while executing — a field the data does not have, a function returning an error — fails its fragment, and the fragment's failure policy applies. Output is buffered, so a template that fails halfway writes nothing rather than half a fragment.
Because every template is in one set, one template can include another by name:
{{template "partials/byline.html" .}}
The names given with {{define}} share that one namespace across every file, so
make them distinctive.
With Root alone, templates are read from disk through os.OpenRoot, so a
symlink leading out of the directory is refused rather than followed: New fails
with ErrTemplateEscapesRoot, naming the file.
Embedding templates in the binary
A binary that reads its templates from ./templates only runs from a directory
that has them. Embed them instead, and it runs from anywhere — a container, a
systemd unit, a copy on a server:
//go:embed all:templates
var templatesFS embed.FS
app, err := collage.New(&collage.Config{
Template: collage.TemplateConfig{
FS: templatesFS,
Root: "templates",
Extension: ".html",
},
})
With FS set, Root is a directory inside that filesystem. It is still stripped
from every name, which is why it is needed: embed.FS names files by their path in
the source tree, and without Root every template would be called
templates/pages/post.html. An empty Root with an FS means the root of the
filesystem. The all: prefix makes the embed include files whose names begin with
. or _, which a plain //go:embed templates would skip.
This is what collage new scaffolds.
Reloading in development
In development — Config.DevMode, or Config.Template.DevMode on its own — every
template is reparsed before each render. Save a template, and the next request uses
it. The framework does not read COLLAGE_DEV: collage dev sets it, and the
scaffolded main.go turns it into Config.DevMode.
An embedded set could not do that: its bytes were fixed when the binary was built,
and reparsing them changes nothing. So in development the directory on disk wins
over the embedded copy whenever Root exists as a directory relative to the
working directory, and the application logs that it chose it. Under collage dev,
which runs in your project directory, it always does.
Two other things make an edit show up at once:
- The page cache is never read in development, so a page cached before the edit does not hide it.
- Every development page carries a small script that reloads the browser when a
template or a static file changes — or a file in a directory named in
Config.DevWatch(since v0.10.0), for content such as Markdown that your handlers read from disk.
Since the whole set is reparsed, a syntax error in any one template fails every render until it is fixed — in development, where you will see it straight away, with the file and line in the error.
What a template receives
. is exactly what the fragment's data handler returned — with
collage.DataHandler, a value of your own type. A fragment without a data handler,
or one adapted with collage.Effect, renders with no data.
type postView struct {
Title string
Published time.Time
Body template.HTML // already sanitised, see below
Tags []string
}
<h1>{{.Title}}</h1>
{{with .Tags}}<p>Tagged {{join ", " .}}</p>{{end}}
A template sees its own fragment's data only. A parent's data is not in scope in a child, and there is no global site object: something every template needs, like the site's name, is data a handler returns or a function you register.
Slots
{{slot "name"}} renders what the fragment's slot of that name holds, in order,
and inserts it as HTML. The children's output is not escaped a second time; each
child escaped its own values when it rendered.
<main>{{slot "content"}}</main>
<aside>{{slot "sidebar"}}</aside>
- A slot that holds nothing renders nothing.
- A name the fragment never declared with
WithSlotis an error that fails the fragment — a typo would otherwise be a section silently missing from the page. - A slot inside
{{if}}that the template skips is not rendered, though its fragments' data handlers were already started; see Fragments and slots.
The slot function belongs to the fragment executing the template, so the same
{{slot "content"}} means a different slot in each fragment that writes it.
Built-in functions
These are available in every template, alongside html/template's own (printf,
len, index, eq and the rest):
| Function | Does |
|---|---|
slot "name" |
Renders the fragments in one of this fragment's slots |
pageURL "name" "param" value … |
The URL of a registered page or document, in this render's locale |
pageURLIn "locale" "name" … |
The same, in exactly the locale given |
localeURL "locale" |
This page in another locale, or empty when it has no path there |
asset "/static/app.css" |
A mounted file's content-addressed URL |
stylesheet "/static/app.css" |
Declares a stylesheet for the page's <head> |
hoist "head" |
Where declarations for an area of the page land |
csrfToken |
The hidden input carrying a form's forgery token |
safeHTML, safeURL |
Mark a string as trusted HTML or URL — escape hatches |
dict "key" value … |
Builds a map, to pass several values to {{template}} |
default fallback value |
value, or fallback when it is empty. Strings only: another type fails the render |
upper, lower, title |
Case conversion |
join sep items |
Joins a []string |
formatTime t layout |
Formats a time.Time with a Go layout |
The link functions are strict: an unknown page name or a missing parameter fails the render instead of producing a broken link. Each function's arguments and behaviour are in Template functions.
Adding your own functions
Config.Template.Funcs adds functions — an html/template FuncMap — to every
template:
import "html/template"
app, err := collage.New(&collage.Config{
Template: collage.TemplateConfig{
FS: templatesFS,
Root: "templates",
Funcs: template.FuncMap{
"money": func(cents int64) string {
return fmt.Sprintf("$%d.%02d", cents/100, cents%100)
},
"readingTime": func(words int) string {
return fmt.Sprintf("%d min read", max(1, words/200))
},
},
},
})
<p>{{money .PriceCents}} · {{readingTime .WordCount}}</p>
- Set it before
New.html/templatecan only call a function whose name was known when the template was parsed, andNewis where parsing happens. A template calling a name nobody registered fails inNew, which is the point: it is a typo you find at startup. - An entry under a built-in's name replaces the built-in — except the eight
that depend on the render:
slot,hoist,asset,stylesheet,csrfToken,pageURL,pageURLInandlocaleURL. The render engine binds its own of each for every render, so overriding any of them is accepted and has no effect. - A function cannot see the request. It is registered once for the whole program. Anything that depends on the request, the locale or the user belongs in the data handler, which is where the data comes from anyway.
A plugin can contribute functions too, from its Configure hook — see
Writing a plugin. When a plugin and Funcs both define a
name, Funcs wins: the application is the one that can see both.
Escaping
html/template escapes every value for the place it appears: HTML text, an
attribute, a URL, inline JavaScript or CSS. A post titled
<script>alert(1)</script> is printed as text, and a javascript: URL in an
href is replaced with a harmless one. You do not escape anything by hand.
Sometimes a value really is HTML — a post body your CMS sanitised, markup your own code produced. There are two ways to say so:
- Give the field the type
template.HTMLin your view, asBodyis above. The type is the decision, made in Go, where it can be reviewed. - Use
safeHTML(orsafeURLfor a URL) in the template.
Either one switches escaping off for that value. Only use them on content you generated or have sanitised, never on anything a user typed; an unescaped user value is a cross-site scripting hole.
Two built-ins produce HTML that is inserted as is:
{{slot}}, whose children escaped their own values.{{hoist}}, which writes what fragments declared for the page. Therc.HoistTitle,rc.HoistMetaand similar helpers escape what they are given; the underlyingrc.Hoistinserts markup exactly as written, so escaping there is yours. See Head and SEO.
pageURL and its siblings escape the parameter values they substitute into a path,
and refuse a value of . or .., which a browser would resolve as a step up the
path.