Twig Builder Reference Since 3.5.0
The builder adapter provides navigation helpers and asset management for Site Builder sites. Navigation functions automatically filter out draft pages and pages with nav set to false, and sort results by the sort field (ascending). Asset functions handle path resolution and cache busting.
Navigation
Section titled “Navigation”Get top-level navigation pages (pages with no parent).
{% set pages = cms.builder.nav() %}{% for p in pages %} <a href="{{ p.route }}">{{ p.title }}</a>{% endfor %}Returns a flat array of page objects from the configured pages collection, filtered to only include pages where:
draftisfalsenavistrue(or missing — defaults totruefor backwards compatibility)parentis empty
Custom Collection
Section titled “Custom Collection”Pass a collection ID to use a different pages collection:
{% set pages = cms.builder.nav('my-custom-pages') %}Return Value
Section titled “Return Value”array — Each element is a page object with all indexed fields:
| Field | Type | Description |
|---|---|---|
id | string | Page identifier |
title | string | Page title |
route | string | URL path (e.g., /about) |
template | string | Template name from builder/pages/ |
layout | string | Layout template name |
description | string | Meta description |
draft | boolean | Always false (drafts are filtered out) |
nav | boolean | Always true (nav-hidden pages are filtered out) |
sort | number | Sort order |
parent | string | Parent page ID (always empty for nav() results) |
subnav()
Section titled “subnav()”Get child pages of a specific parent.
{% set children = cms.builder.subnav('blog') %}{% for p in children %} <a href="{{ p.route }}">{{ p.title }}</a>{% endfor %}Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
parentId | string | yes | The id of the parent page |
collection | string | no | Custom collection ID (defaults to configured pages collection) |
Example: Section Sub-Navigation
Section titled “Example: Section Sub-Navigation”{# Main nav #}<nav> {% for p in cms.builder.nav() %} <a href="{{ p.route }}">{{ p.title }}</a> {% endfor %}</nav>
{# Sub-nav for the current section #}{% set children = cms.builder.subnav('services') %}{% if children is not empty %}<nav class="subnav"> {% for p in children %} <a href="{{ p.route }}">{{ p.title }}</a> {% endfor %}</nav>{% endif %}navTree()
Section titled “navTree()”Get the full navigation hierarchy as a nested tree.
{% set tree = cms.builder.navTree() %}Returns top-level pages with a children key containing their child pages, recursively nested.
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
collection | string | no | Custom collection ID (defaults to configured pages collection) |
Return Structure
Section titled “Return Structure”Each page in the tree has all the standard page fields plus a children array:
[ {id: "home", title: "Home", route: "/", children: []}, {id: "services", title: "Services", route: "/services", children: [ {id: "web-design", title: "Web Design", route: "/services/web-design", children: []}, {id: "seo", title: "SEO", route: "/services/seo", children: []}, ]}, {id: "about", title: "About", route: "/about", children: []},]Example: Two-Level Navigation
Section titled “Example: Two-Level Navigation”<nav> {% for p in cms.builder.navTree() %} <a href="{{ p.route }}">{{ p.title }}</a> {% if p.children is not empty %} <ul> {% for child in p.children %} <li><a href="{{ child.route }}">{{ child.title }}</a></li> {% endfor %} </ul> {% endif %} {% endfor %}</nav>Example: Recursive Navigation Macro
Section titled “Example: Recursive Navigation Macro”For deeply nested menus, use a Twig macro:
{% macro navItems(pages) %} {% for p in pages %} <li> <a href="{{ p.route }}">{{ p.title }}</a> {% if p.children is not empty %} <ul> {{ _self.navItems(p.children) }} </ul> {% endif %} </li> {% endfor %}{% endmacro %}
<nav> <ul> {{ _self.navItems(cms.builder.navTree()) }} </ul></nav>Assets
Section titled “Assets”asset()
Section titled “asset()”Resolve an asset path to a URL with automatic cache busting.
{{ cms.builder.asset('images/hero.webp') }}{# Output: /assets/images/hero.webp?v=1714300000 #}Use this when you need the raw URL — for background images, srcset, custom attributes, or any case where css()/js() don’t fit.
How Resolution Works
Section titled “How Resolution Works”- Manifest exists — reads
manifest.jsonfrom the assets directory and resolves hashed filenames (e.g.,style.css→style.a1b2c3.css) - No manifest, file exists — appends
?v={mtime}for cache busting via file modification time - File not found — returns the raw path with no cache busting
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | yes | Asset path relative to the assets directory |
Output a <link rel="stylesheet"> tag for a CSS file.
{{ cms.builder.css('style.css') }}{{ cms.builder.css('vendor/normalize.css') }}Output:
<link rel="stylesheet" href="/assets/style.css?v=1714300000"><link rel="stylesheet" href="/assets/vendor/normalize.css?v=1714300000">Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | yes | CSS file path relative to the assets directory |
Output a <script> tag for a JavaScript file.
{{ cms.builder.js('app.js') }}{{ cms.builder.js('app.js', {module: true}) }}Output:
<script src="/assets/app.js?v=1714300000"></script><script type="module" src="/assets/app.js?v=1714300000"></script>Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | yes | JS file path relative to the assets directory |
options | object | no | Options: module (bool) adds type="module" attribute |
preload()
Section titled “preload()”Output a <link rel="preload"> tag for preloading assets. Automatically adds the crossorigin attribute for fonts (required by browsers).
{{ cms.builder.preload('fonts/inter.woff2', 'font') }}{{ cms.builder.preload('hero.webp', 'image') }}{{ cms.builder.preload('app.js', 'script') }}Output:
<link rel="preload" href="/assets/fonts/inter.woff2?v=1714300000" as="font" crossorigin><link rel="preload" href="/assets/hero.webp?v=1714300000" as="image"><link rel="preload" href="/assets/app.js?v=1714300000" as="script">Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | yes | Asset path relative to the assets directory |
as | string | yes | Resource type: font, image, script, style, fetch |
Asset Configuration
Section titled “Asset Configuration”Assets Directory
Section titled “Assets Directory”By default, assets are served from the assets/ directory in your docroot. Configure a different path in Admin > Settings > Builder:
| Setting | Type | Default | Description |
|---|---|---|---|
| Assets Path | text | assets | Public assets directory relative to docroot |
Your build tool should output files to this directory. The web server (Apache/Nginx) serves them as static files — T3 only generates the URLs.
Build Tool Manifest
Section titled “Build Tool Manifest”For production builds with content-hashed filenames, output a manifest.json to your assets directory. The asset functions will automatically resolve hashed filenames from the manifest.
export default { build: { manifest: true, outDir: 'assets' }}esbuild
Section titled “esbuild”require('esbuild').build({ entryPoints: ['src/app.js', 'src/style.css'], outdir: 'assets', metafile: true, // Use a plugin to write manifest.json})When a manifest is present, hashed filenames are used instead of mtime query strings:
<!-- Without manifest --><link rel="stylesheet" href="/assets/style.css?v=1714300000">
<!-- With manifest --><link rel="stylesheet" href="/assets/style.a1b2c3.css">Templates don’t change between development and production — the asset functions handle resolution automatically.
Example: Complete Layout
Section titled “Example: Complete Layout”<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{% block title %}{{ page.title }}{% endblock %}</title>
{{ cms.builder.preload('fonts/inter.woff2', 'font') }} {{ cms.builder.css('style.css') }}</head><body> {% include 'partials/nav.twig' %}
<main>{% block content %}{% endblock %}</main>
{% include 'partials/footer.twig' %}
{{ cms.builder.js('app.js', {module: true}) }}</body></html>