Layouts and Partials
Page chrome, partials, template context, and the helper toolbox.
Layouts
Layouts live in layouts/*.html and are referenced by slug from theme.json's layouts[] array. Each entry carries { slug, name, file, is_default?, field_schema? }. The slug picked for a node follows the cascade described in the themes overview; the resolved layout's template is read, parsed with the renderer's FuncMap, and rendered with a layout-scoped context.
A layout's body must include {{.node.blocks_html}} somewhere — that's the rendered block sequence for the current node. Most layouts also call {{renderLayoutBlock "site-header"}} and {{renderLayoutBlock "site-footer"}} for the chrome partials so the layout itself stays small enough to read.
Layout context
Inside a layout you have:
.app— site-wide context:HeadStyles,HeadScripts,FootScripts,HeadMeta,BlockStyles,BlockScripts(rendered HTML to splice into<head>/<body>);Settings(a map indexable with thesetting/mustSettinghelpers),Languages(the configured language set),CurrentLang(the current request's language object),Menus(a map keyed by menu slug),ThemeURL(the URL prefix for theme assets, conventionally/theme)..node— current node:ID,Status,Title,Slug,FullURL,FeaturedImage,Excerpt,Taxonomies(a map oftaxonomy_slug → [terms]),BlocksHTML(the pre-rendered blocks),Fields(the node'sfields_data),SEO(the per-node SEO overrides),NodeType,LanguageCode,Translations(sibling-language counterparts)..user— viewer info:LoggedIn,ID,Email,Role,FullName. Anonymous visitors getLoggedIn=falseand zero values elsewhere; never gate a private resource on.useralone, the kernel does that with auth middleware before the template ever runs..theme_settings— a map keyed by settings-page slug, each value a map of field key → stored value for the active theme.
Partial context
A partial sees the full layout context plus .partial.* — its own field values resolved per-page from the node's layout_data[partial_slug] map with a fallback chain: explicit value on the node → the partial's field_schema default → the partial's optional default_from reference (which can pull from another partial or a theme setting) → blank. Use partials for chrome that varies subtly per page (e.g. a hero override on the homepage but not on blog posts).
Block view context
Inside a block view.html you see only the block's own field values at root. There is no .app, no .node, no .user. The isolation is intentional: blocks are atomic, cacheable, and have to render correctly in core.render.block previews where no node or user exists. Theme settings are reachable via the themeSetting and themeSettingsPage template helpers, which the renderer injects into the per-block FuncMap so a block can pull theme-wide configuration without needing the page context.
fields_data from the seed or the editor — copy the URL, the title, the image, whatever you need, into the block instance's fields. Or render the block at the layout level via renderLayoutBlock and treat it as a partial. Don't try to escape the isolation; you'll trip over the cache and the standalone preview path.Settings access
Site settings are stored with dotted keys (e.g. squilla.brand.name, seo.default_og_image). Go templates can't dot-traverse arbitrary keys with .foo.bar when foo contains a literal dot, so use the setting helper or index:
{{- $s := .app.settings -}}
<title>{{ index $s "squilla.brand.name" }}</title>
{{/* dedicated helper, equivalent */}}
<meta name="description" content="{{ setting .app.settings "squilla.brand.tagline" }}">
{{/* mustSetting raises a render error if the key is missing */}}
<meta name="theme-color" content="{{ mustSetting .app.settings "squilla.brand.theme_color" }}">
Template function reference
The renderer (internal/rendering/template_renderer.go) registers a FuncMap before parsing every template. The full catalogue:
setting,mustSetting— read site settings from.app.settings(mustSettingraises a render error if missing).themeSetting,themeSettingsPage— read theme-scoped settings (in block context where.theme_settingsisn't available).renderLayoutBlock— inject a partial. Recursion-bounded at depth 5 to prevent self-referential loops.filter— apply a Tengo filter chain to a value:{{ filter "node.title" .node.Title }}. Returns the post-filter value.event— fire an event-with-result and inline the returned HTML. Used for forms:{{ event "forms:render" (dict "form_id" "contact") }}calls the forms-extensionHandleEventRPC and splices the rendered form HTML into the page.image_url— resolve a media URL plus a size name to the size-specific URL ({{ image_url .featured.src "hero" }}). The size must be declared intheme.json'simage_sizes[]; missing sizes return the original URL.image_srcset— build a responsivesrcsetstring from a media URL and one or more size names:{{ image_srcset .image.src "hero" "hero-2x" }}.safeHTML,raw,safeURL— bypass auto-escaping. The HTML sanitiser still runs upstream for richtext fields, so this is safe for kernel-rendered HTML; do not use it on raw user input you haven't sanitised yourself.json— marshal a value as JSON for inline data attributes or<script type="application/json">blocks.dict,list,seq— small data utilities for building maps/slices inline (e.g. for passing options torenderLayoutBlock).split,trim,lastWord,beforeLastWord— string helpers used by typography flourishes (e.g. styling the last word of a heading).add,sub,mod— arithmetic.deref— dereference a pointer to its underlying value (mostly forFeaturedImageon nodes, which is*MediaFile).
Menus inside templates
Menus are exposed as .app.menus["slug"] with an items array. Each item carries title, url, target, optional children, and an active boolean for the current request URL. The data shape is the same the admin UI emits and what core.menu.upsert normalises its input into.
{{- $nav := index .app.menus "main-nav" -}}
<nav>
{{- range $nav.items -}}
<a href="{{ .url }}"
{{ if .target }}target="{{ .target }}"{{ end }}
{{ if .active }}aria-current="page"{{ end }}>
{{ .title }}
</a>
{{- end -}}
</nav>
.title, NOT .label. The Tengo input to core/menus.upsert uses label:, but the rendered shape uses title (so HTML title attributes also map). Templates that read {{ .label }} will silently render an empty string. core.guide has this fact in its gotchas table under menu_label_vs_title.Layout <head> checklist
Every layout's <head> should include the four kernel-managed fragments so SEO, fonts, and block-scoped CSS land correctly. The order matters: write the SEO meta first (so any later <meta> can override), the theme's bundle next, the block-scoped CSS after that (so block styles win the cascade), and finally any head-injected scripts.
<head>
{{ safeHTML .app.head_meta }} {{/* meta + canonical + og + twitter, emitted by seo-extension */}}
{{ safeHTML .app.head_styles }} {{/* theme CSS bundles, declared in theme.json's styles[] */}}
{{ safeHTML .app.block_styles }} {{/* per-block scoped CSS, deduped */}}
{{ safeHTML .app.head_scripts }} {{/* head-injected JS, e.g. analytics */}}
</head>
404 and error layouts
If your theme ships layouts/404.html registered as { "slug": "404", "file": "404.html" }, the kernel uses it for unmatched routes. The layout receives the standard layout context with .node set to a synthetic 404 node — use the same {{.node.blocks_html}} for the body so editors can stage 404 content like any other page. Without a registered 404 layout the kernel falls back to a built-in HTML5 skeleton with a one-line message; core.theme.checklist warns about this so you can ship a themed 404 before users hit it.
The error slug is similar: a layout registered as { "slug": "error", "file": "error.html" } is rendered for unhandled 5xx errors. The same fallback rules apply.