๐Ÿ“ 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:

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:

FieldTypeDescription
.StatusCodeuint16HTTP status code (e.g. 404)
.MessagestringShort status text (e.g. Not Found)
.DescriptionstringLonger description (e.g. The server can not find the requested page)
.OriginalURIstringRequest URI that caused the error *
.NamespacestringKubernetes namespace of the backend service *
.IngressNamestringName of the Ingress resource *
.ServiceNamestringName of the backend service *
.ServicePortstringPort of the backend service *
.RequestIDstringUnique request ID *
.ForwardedForstringOriginal client IP(s) from X-Forwarded-For *
.HoststringRequest Host header *
.HomepageURLstringHomepage URL set via --homepage-url (empty if not configured)
.Links[]LinkExtra links set via --add-link (empty slice if not configured)
.Config.ShowRequestDetailsboolWhether --show-details is enabled
.Config.L10nDisabledboolWhether --disable-l10n is set

* - Requires --show-details

Each element of .Links has the following sub-fields:

Sub-fieldTypeDescription
.LabelstringLink text
.URLstringTarget 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 }}.

FunctionDescriptionExample
nowCurrent time (time.Time){{ now.Format "2006-01-02" }} / {{ now.Unix }}
hostnameServer hostname{{ hostname }}
versionApplication version string{{ version }}
env "KEY"Env var value (sensitive keys masked with ***){{ env "STAGE" }}
toJson / toJSONJSON-encode a value{{ .Message | toJson }}
toInt / intConvert to integer{{ .StatusCode | int }}
toString / strConvert to string{{ .StatusCode | str }}
escapeHTML-escape{{ .OriginalURI | escape }}
urlEncodeURL-encode{{ .OriginalURI | urlEncode }}
trimStrip leading/trailing whitespace{{ .Message | trim }}
trimPrefixRemove prefix{{ .Message | trimPrefix "Error: " }}
trimSuffix / trimPostfixRemove suffix{{ .Message | trimSuffix "!" }}
trimAllStrip specific characters{{ ".test." | trimAll "." }}
lower / upperChange case{{ .Message | upper }}
replaceReplace all occurrences{{ .Message | replace " " "_" }}
containsSubstring check{{ .Message | contains "test" }}...{{ end }}
hasPrefix / hasSuffixPrefix/suffix check{{ .Message | hasPrefix "test" }}...{{ end }}
splitSplit string by separator{{ split ";" "a;b;c" }}
joinJoin slice with separator{{ split ";" "a;b;c" | join ", " }}
fieldsSplit string by whitespace{{ fields "foo bar baz" | join "-" }}
substrSubstring by rune index and length{{ "Hello, World!" | substr 7 5 }}
truncateTruncate with ... appended{{ .Description | truncate 80 }}
repeatRepeat string N times{{ "Ha" | repeat 3 }}
quote / squoteWrap in double/single quotes{{ .Message | quote }}
countCount substring occurrences{{ "test" | count "t" }}
defaultFallback value for empty input{{ .OriginalURI | default "N/A" }}
coalesceFirst non-empty value from a list{{ coalesce .Message .Description "error" }}
ternaryInline conditional{{ .Config.ShowRequestDetails | ternary "shown" "hidden" }}
isEmpty / isNotEmptyEmptiness check{{ if isNotEmpty .Description }}...{{ end }}
l10nScriptInline 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:

  1. Add data-l10n attributes to elements whose text content should be translated
  2. 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.