๐ Templating and Localization
May 4, 2026 ยท View on GitHub
Templates are parsed once at startup. The same template engine is used for all output formats. When a custom
HTML template is set via --html-template, the --template-name and --rotation-mode flags are ignored.
Go template primer
Error pages uses the standard Go text/template package (with HTML output treated as text to
preserve full control over markup). If you have never written a Go template before, do not worry - it is genuinely one
of the simplest templating languages around. The full mental model fits in a few minutes.
Key concepts:
{{ }}- action delimiters; everything outside them is emitted verbatim.{{ .Field }}- output a field from the data object (.is "current value").{{ if .Cond }} ... {{ else }} ... {{ end }}- conditional.{{ range .Slice }} {{ . }} {{ end }}- iteration (.becomes each element).{{ .Value | funcName }}- pipeline; passes the value on the left as the last argument to the function on the right.{{ /* comment */ }}- comment (not emitted).
Documentation:
text/templatepackage - the complete, authoritative reference.- Go Templates Guide - syntax explained with examples (recommended for beginners).
Warning
{{ and }} are reserved as Go template delimiters - any literal occurrence causes a parse error. This is
common in JSDoc type annotations (/** @param {{ id: number }} โ */), CSS, etc. To work around this, you can simply
add a single space between the braces: /** @param { { id: number } } โ
*/.
Template data
All templates receive a data object with the following fields:
| Field | Type | Description |
|---|---|---|
.StatusCode | uint16 | HTTP status code (e.g. 404) |
.Message | string | Short status text (e.g. Not Found) |
.Description | string | Longer description (e.g. The server can not find the requested page) |
.OriginalURI | string | Request URI that caused the error * |
.Namespace | string | Kubernetes namespace of the backend service * |
.IngressName | string | Name of the Ingress resource * |
.ServiceName | string | Name of the backend service * |
.ServicePort | string | Port of the backend service * |
.RequestID | string | Unique request ID * |
.ForwardedFor | string | Original client IP(s) from X-Forwarded-For * |
.Host | string | Request Host header * |
.HomepageURL | string | Homepage URL set via --homepage-url (empty if not configured) |
.Links | []Link | Extra links set via --add-link (empty slice if not configured) |
.Config.ShowRequestDetails | bool | Whether --show-details is enabled |
.Config.L10nDisabled | bool | Whether --disable-l10n is set |
*- Requires--show-details
Each element of .Links has the following sub-fields:
| Sub-field | Type | Description |
|---|---|---|
.Label | string | Link text |
.URL | string | Target URL |
Example usage in a custom template:
{{ if .Links }}
<nav>
{{ range .Links }}<a href="{{ .URL }}">{{ .Label }}</a>{{ end }}
</nav>
{{ end }}
In addition to the fields above, templates also have access to a set of built-in functions (see below), which are
pipeline-friendly (needle before haystack): {{ .Message | default "Unknown" | upper }}.
| Function | Description | Example |
|---|---|---|
now | Current time (time.Time) | {{ now.Format "2006-01-02" }} / {{ now.Unix }} |
hostname | Server hostname | {{ hostname }} |
version | Application version string | {{ version }} |
env "KEY" | Env var value (sensitive keys masked with ***) | {{ env "STAGE" }} |
toJson / toJSON | JSON-encode a value | {{ .Message | toJson }} |
toInt / int | Convert to integer | {{ .StatusCode | int }} |
toString / str | Convert to string | {{ .StatusCode | str }} |
escape | HTML-escape | {{ .OriginalURI | escape }} |
urlEncode | URL-encode | {{ .OriginalURI | urlEncode }} |
trim | Strip leading/trailing whitespace | {{ .Message | trim }} |
trimPrefix | Remove prefix | {{ .Message | trimPrefix "Error: " }} |
trimSuffix / trimPostfix | Remove suffix | {{ .Message | trimSuffix "!" }} |
trimAll | Strip specific characters | {{ ".test." | trimAll "." }} |
lower / upper | Change case | {{ .Message | upper }} |
replace | Replace all occurrences | {{ .Message | replace " " "_" }} |
contains | Substring check | {{ .Message | contains "test" }}...{{ end }} |
hasPrefix / hasSuffix | Prefix/suffix check | {{ .Message | hasPrefix "test" }}...{{ end }} |
split | Split string by separator | {{ split ";" "a;b;c" }} |
join | Join slice with separator | {{ split ";" "a;b;c" | join ", " }} |
fields | Split string by whitespace | {{ fields "foo bar baz" | join "-" }} |
substr | Substring by rune index and length | {{ "Hello, World!" | substr 7 5 }} |
truncate | Truncate with ... appended | {{ .Description | truncate 80 }} |
repeat | Repeat string N times | {{ "Ha" | repeat 3 }} |
quote / squote | Wrap in double/single quotes | {{ .Message | quote }} |
count | Count substring occurrences | {{ "test" | count "t" }} |
default | Fallback value for empty input | {{ .OriginalURI | default "N/A" }} |
coalesce | First non-empty value from a list | {{ coalesce .Message .Description "error" }} |
ternary | Inline conditional | {{ .Config.ShowRequestDetails | ternary "shown" "hidden" }} |
isEmpty / isNotEmpty | Emptiness check | {{ if isNotEmpty .Description }}...{{ end }} |
l10nScript | Inline the localization JS script | <script>{{ l10nScript }}</script> |
Note
env masks values whose key (split by _) contains PASSWORD, SECRET, KEY, TOKEN, PASS, PWD,
or CRED (case-insensitive). Those calls return a string of * characters instead of the actual value.
Localization
HTML pages support automatic client-side localization in 15+ languages, templates that want l10n must:
- Add
data-l10nattributes to elements whose text content should be translated - Include
<script>{{ l10nScript }}</script>in the HTML
The browser detects the visitor's preferred language via navigator.languages and translates all [data-l10n]
elements in-place - no server round-trip required. Localization can be disabled globally with --disable-l10n
(or via env DISABLE_L10N=true).
You can find more details on handling localization in templates in the Localization documentation.