Reference
Configuration
Every field of collage.Config and its sub-structs, with its default and what validation checks.
An application is configured with one collage.Config value, handed to
collage.New. Every field has a usable zero value, so a configuration only says
what differs from the defaults:
app, err := collage.New(&collage.Config{
Template: collage.TemplateConfig{Root: "templates"},
})
New does three things with it, in order:
- Fills in defaults. It calls
cfg.ApplyDefaults(), which sets every zero-valued field that has a default. It changes the value you passed, so you can read back exactly what the application was built with. - Validates. It calls
cfg.Validate()and returns the first problem as an error — see Validation. - Builds the application. A template root that does not exist, a template that
does not parse, a plugin's
Configurefailing — each is reported here, not at the first request.
Passing nil is ErrNilConfig. A nil configuration is a mistake rather than a
request for defaults, because the configuration says where your templates are.
Config
| Field | Type | Default | Meaning |
|---|---|---|---|
DevMode |
bool |
false |
Development mode across the framework. |
DevWatch |
[]string |
none | Extra directories whose changes reload a development page. Since v0.10.0. |
Logger |
*slog.Logger |
chosen at startup | The logger the framework and plugins write through. |
Server |
ServerConfig |
The HTTP server. | |
Security |
SecurityConfig |
Request-forgery protection. | |
Template |
TemplateConfig |
Template loading and rendering. | |
Cache |
CacheConfig |
The page cache. | |
Locale |
LocaleConfig |
Which locales URLs carry. | |
TrailingSlash |
bool |
false |
End every page's URL in /. Since v0.13.0. |
Observability |
ObservabilityConfig |
Metrics and tracing. | |
Plugins |
[]Plugin |
none | Plugins registered while the application is built. |
PluginConfig |
map[string]json.RawMessage |
none | Each plugin's own configuration, keyed by plugin name. |
DevMode
Turns on development mode: templates reloaded from disk before every render, pages
that reload themselves in the browser, failed fragments shown on the page, the
page cache never read from, a disk cache replaced by memory, directories named in
DevWatch watched, and the built-in error page naming the fragment where a
failure started and showing the full error chain. It must be off in
production — those diagnostics carry paths, hostnames and whatever else error
messages contain.
The effective value is cfg.IsDevMode(), which is DevMode || Template.DevMode.
A scaffolded project sets it from COLLAGE_DEV=1, which collage dev sets for you.
DevWatch
Directories, besides the templates and the mounts, whose changes reload a development page — for content your application reads from disk itself, such as Markdown or JSON. Since v0.10.0.
app, err := collage.New(&collage.Config{
DevMode: os.Getenv("COLLAGE_DEV") == "1",
DevWatch: []string{"content"},
// ...
})
A directory that does not exist is skipped rather than an error, because the same configuration runs wherever the binary is started. It is ignored outside development. Watching a directory only reloads the browser; reading the new content on the next render — rather than a copy loaded at startup — is up to your code.
Logger
nil, the default, lets the framework choose when it builds the application: on a
terminal — and only when nothing has replaced slog's default handler — a compact
handler meant for a person, one line per record with a coloured level marker.
Anywhere else it is slog.Default(), unchanged. An application that called
slog.SetDefault keeps its handler. Pass a logger to be certain, such as a JSON
handler for logs a machine reads. ApplyDefaults leaves this field nil.
Plugins and PluginConfig
Plugins are registered in order while New runs. A plugin that must act before
templates are parsed — to add a template function or wrap mounts — has to arrive
here rather than through app.RegisterPlugin.
PluginConfig holds each plugin's section as raw JSON. The framework reads no file:
fill it however you like, or use collage.LoadPluginConfig("plugins-config.json"),
which returns nil for a missing file. A key that names no registered plugin stops
the application from starting with ErrUnknownPluginConfig. See
Using plugins.
TrailingSlash
Every page has one address, and TrailingSlash says whether it ends in /. On,
/blog/hello/ is the page and /blog/hello redirects to it with a 301; off,
which is the default, it is the other way round. Links built by name —
pageURL, pageURLIn, localeURL, app.URL — come out in the chosen spelling,
a locale's home included: /tr/ on, /tr off.
Turn it on for a site you export to a static host.
The export writes a page as <path>/index.html, which a host serves at
/blog/hello/ and reaches from /blog/hello only through a redirect — so with it
off, every canonical link, sitemap entry and internal link points at a redirect.
It applies to pages. A document is a file and keeps its path as
written, /sitemap.xml either way, and an action is answered at whichever
spelling it was posted to.
ServerConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
Host |
string |
"localhost" |
Address the server listens on. |
Port |
int |
3000 |
TCP port, 1–65535. |
ReadTimeout |
time.Duration |
15s |
How long reading a request may take. |
WriteTimeout |
time.Duration |
30s |
How long writing a response may take. |
IdleTimeout |
time.Duration |
60s |
How long a keep-alive connection may sit idle. |
ShutdownTimeout |
time.Duration |
10s |
How long graceful shutdown waits for in-flight requests. |
MaxBodyBytes |
int64 |
4 MiB | Bound on an action's request body when the action sets none. |
Host defaults to localhost, which is unreachable from outside the machine — in
a container, set it to 0.0.0.0. MaxBodyBytes is not filled in by
ApplyDefaults: zero means the built-in 4 MiB (4 << 20 bytes), applied when a
request arrives, and a negative value means unbounded. An unbounded body is memory
an anonymous caller chooses the size of, so choose that deliberately. An action can
set its own bound with WithMaxBodyBytes; see
Forms and actions.
SecurityConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
CSRFKey |
[]byte |
generated per process | Key that signs forgery tokens. |
CSRFCookieName |
string |
"collage_csrf" |
Cookie a token is carried in. |
CSRFFieldName |
string |
"_csrf" |
Form field a token is submitted in. |
CSRFHeaderName |
string |
"X-CSRF-Token" |
Header a token may be submitted in instead. |
DisableCSRF |
bool |
false |
Turns forgery checking off for the whole application. |
Set CSRFKey before deploying anything with a form. It should be at least 32
random bytes, kept with your other secrets and the same on every instance. Left
empty, a key is generated for each process: fine for a first run, wrong to deploy,
because a token issued before a restart — or by another instance — is refused. The
application logs that it generated one — a warning outside development, at info
level in it — and only when some action answers an unsafe method (POST, PUT, PATCH,
DELETE) and is not exempted with WithoutCSRF, since only such an action ever
verifies a token (the WithoutCSRF exemption since v0.11.0). The key is not part
of the disk cache's namespace, so the cache survives a new key; a cached page with
a form in it, stored under the old one, is rendered again rather than served — see
Caching. The scaffolded main.go reads it from
COLLAGE_CSRF_KEY; openssl rand -hex 32 makes one.
The name defaults above are applied by the forgery guard, not by ApplyDefaults,
so the fields stay empty in your Config. CSRFFieldName renames the field on
both sides: {{csrfToken}} writes the name the check reads, so forms need no
change. Script that reads the token from the form — input[name="_csrf"] — has
to use the new name.
DisableCSRF is for an application with no browser-submitted forms at all — an API
behind its own authentication. With it set, {{csrfToken}} fails the render rather
than rendering a form whose submission would mean nothing. To exempt one action
instead, use WithoutCSRF on it.
TemplateConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
FS |
fs.FS |
nil |
Filesystem templates are loaded from; nil means disk. |
Root |
string |
"./templates" on disk |
Directory templates are loaded from, stripped from every template name. |
Extension |
string |
".html" |
File extension of template files. |
Funcs |
template.FuncMap |
none | Extra template functions, merged over the built-ins. |
DevMode |
bool |
false |
Reload templates from disk on every render. |
Timeout |
time.Duration |
5s |
Default data-handler timeout, and the whole budget of every document handler. |
FS and Root
With FS nil, Root is a path on disk, relative to the working directory, and
defaults to ./templates. With FS set, Root is a slash-separated directory
inside it and is not defaulted — an empty Root means the root of FS itself.
Embedding is what lets a binary run from any directory:
//go:embed all:templates
var templates embed.FS
app, err := collage.New(&collage.Config{
Template: collage.TemplateConfig{FS: templates, Root: "templates"},
})
In development, an embedded set cannot change, so collage prefers the directory on
disk when it is there. A Root that does not exist is ErrTemplateRootMissing
from New, and a template under it that resolves outside it — through a symlink —
is ErrTemplateEscapesRoot.
Funcs
Added to the built-in functions when the templates are parsed; an entry under a
built-in name replaces that built-in, and an entry under a name a plugin added
replaces the plugin's. It must be set before New, because a template can only
call a function that existed when it was parsed — a template calling an unknown
name fails in New. The functions bound per render (slot, hoist, asset,
stylesheet, csrfToken, pageURL, pageURLIn, localeURL) are rebound on every
render, so overriding them has no effect. See
Template functions.
Timeout
The deadline for a data handler whose fragment sets none with WithTimeout. It is
also the only bound on a document handler, which has no timeout
of its own — so raising it for one slow fragment raises it for every sitemap and
feed. Like every timeout in collage it bounds the context the handler receives; a
handler that never checks ctx.Done() can run past it.
CacheConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
Enabled |
bool |
false |
Master switch for the page cache. |
Store |
Cache |
nil |
Your own cache implementation. |
Type |
string |
"memory" when enabled |
Built-in cache when Store is nil: "memory" or "disk". |
DefaultTTL |
time.Duration |
5m |
Entry lifetime when a page sets none. |
MaxEntries |
int |
10000 |
Cap on cache entries; negative means unlimited. |
Dir |
string |
none | Where a disk cache stores entries. Required for "disk". |
Version |
string |
derived from the binary | Identifies the build a disk cache's entries belong to. |
MaxKeysPerTag |
int |
10000 |
Cap on cache keys recorded under one tag; negative means unlimited. |
Caching is off by default. With Enabled false nothing is cached, even with a
Store set. Type defaults to "memory" only when Enabled is true and Store
is nil.
A Store replaces Type entirely, including in validation. Implement
collage.Cache (Get, Set, Invalidate, InvalidateKey, Clear) to put pages
in Redis or anywhere else; a store that also implements collage.TaggedCache is
handed each entry's tags on write. Leave the field unset rather than assigning a
nil pointer of a concrete type, which is a non-nil interface that panics on the
first lookup.
A disk cache outlives the process. Its entries live in a subdirectory of Dir
named for a hash of Version, so a new build reads a different directory and finds
nothing stale. Leave Version empty and it is a hash of the running executable,
which changes exactly when the output might; set it — a commit, a release tag —
when something outside the binary decides what pages look like. If the executable
cannot be hashed, an in-memory cache is used instead, with a warning — and so,
since v0.11.0, when the directory cannot be created, a read-only filesystem say:
collage.New warns and carries on with memory rather than failing. A write that
fails once the application is running is logged, and the page is served uncached.
In development a disk cache is never used: an in-memory one stands in for it.
MaxEntries also bounds the values collage.Cached keeps across requests.
MaxKeysPerTag bounds the framework's dependency tracker, the per-process index
that maps a tag back to cache keys. Every distinct query string is a distinct key,
and nothing removes a key from the tracker when the cache evicts or expires its
entry, so without a cap a client could grow that index without limit. When a tag is
at the cap, its oldest key is dropped from the tracker only, not from the cache.
What that costs depends on the store. The built-in memory and disk caches index
tags themselves (they implement TaggedCache), so InvalidateTags still reaches
every entry they hold; only the count InvalidateTagsN reports, which is what the
tracker resolved, can come out lower. A custom Store that does not implement
TaggedCache relies on the tracker alone, and for it a dropped key is an entry
InvalidateTags no longer reaches — served until it expires. With such a store,
set the cap above the number of live entries any one tag can cover.
Zero for either cap means the default; only a negative value means unlimited. See Caching.
LocaleConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
Default |
string |
"en" |
Locale of a URL with no locale prefix. |
Supported |
[]string |
[Default] |
Every locale the application serves. |
DisablePathLocale |
bool |
false |
Stop resolving the locale from the path; every request is in Default. |
PrefixDefault |
bool |
false |
Give Default's pages a prefix too: /en/about. Since v0.14.0. |
The URL is the only thing that selects a locale: /about is in Default, and
/tr/hakkinda is in "tr". collage never picks one from Accept-Language or a
cookie, because a URL that means different things to different readers is one
caches, crawlers and shared links all get wrong. /en/about, the default
locale's own prefix, redirects permanently to /about — unless PrefixDefault is
set, which makes /en/about the address and redirects /about to it; see
Links and locales. To negotiate, do it in
middleware: redirect a Turkish browser to /tr, or render one URL per language
and declare it with collage.Vary. See
Links and locales.
ObservabilityConfig
| Field | Type | Default | Meaning |
|---|---|---|---|
Metrics |
Metrics |
no-op | Receives counters and timings. |
Tracer |
Tracer |
no-op | Starts spans around requests, renders and fragments. |
Both are interfaces you implement to bridge collage into your backend, and nil
means a no-op.
type Metrics interface {
RenderDuration(ctx context.Context, page string, d time.Duration, cacheHit bool)
FragmentDuration(ctx context.Context, page, fragment string, d time.Duration, err error)
CacheEvent(ctx context.Context, event collage.CacheEvent, key string)
HTTPResponse(ctx context.Context, status int, path string, d time.Duration)
Invalidation(ctx context.Context, tags []string, keys int)
}
type Tracer interface {
StartSpan(ctx context.Context, name string) (context.Context, collage.Span)
}
type Span interface {
SetAttribute(key, value string)
RecordError(err error)
End()
}
CacheEvent is one of collage.CacheHit, CacheMiss, CacheSet, CacheEvict,
CacheInvalidate and CacheCoalesced — the last meaning a request was served by a
render of the same key that was already running.
Validation
Validate returns the first of these it finds, after ApplyDefaults has run:
| Condition | Error |
|---|---|
Server.Port outside 1–65535 |
ErrInvalidPort |
Template.Root empty and Template.FS nil |
ErrEmptyTemplateRoot |
Cache.Enabled, no Store, and Type neither "memory" nor "disk" |
ErrInvalidCacheType |
Cache.Enabled, no Store, Type "disk", and Cache.Dir empty |
ErrEmptyCacheDir |
Locale.Default empty |
ErrEmptyLocaleDefault |
Locale.Default not in Locale.Supported |
ErrLocaleDefaultNotSupported |
A negative Server.ReadTimeout, WriteTimeout, IdleTimeout, ShutdownTimeout, Template.Timeout or Cache.DefaultTTL |
ErrNegativeDuration |
The negative-duration error is one sentinel for all six fields; the message names
the one that failed, such as server.read_timeout. Because defaults are applied
first, a zero port or an empty default locale never reaches validation from New
— only a value you set explicitly can fail. Match these with errors.Is; the full
list is in Errors.
You can call both yourself — to check a configuration in a test, say:
cfg := collage.Config{Server: collage.ServerConfig{Port: 70000}}
cfg.ApplyDefaults()
err := cfg.Validate() // wraps collage.ErrInvalidPort
ApplyDefaults is idempotent: applying it to a configuration that already has
defaults changes nothing.
A complete configuration
The configuration a scaffolded project starts from, with the parts that vary by environment read from it:
pluginConfig, err := collage.LoadPluginConfig("plugins-config.json")
if err != nil {
return nil, fmt.Errorf("plugin configuration: %w", err)
}
app, err := collage.New(&collage.Config{
DevMode: os.Getenv("COLLAGE_DEV") == "1",
Server: collage.ServerConfig{
Host: envString("HOST", "localhost"),
Port: envInt("PORT", 3000),
},
Template: collage.TemplateConfig{
FS: templatesFS,
Root: "templates",
Extension: ".html",
},
Cache: collage.CacheConfig{
Enabled: true,
Type: "disk",
Dir: ".cache",
DefaultTTL: 5 * time.Minute,
},
PluginConfig: pluginConfig,
Security: collage.SecurityConfig{
CSRFKey: []byte(os.Getenv("COLLAGE_CSRF_KEY")),
},
})
envString and envInt are two small helpers in the scaffolded main.go that
read a variable and fall back to a default.