Navigation
How menus work: navigation slots declared in site.yaml, navigation sets edited in the admin or seeded from files, the nav global and the nav tag in templates, and breadcrumbs, child pages and language switchers.
A site’s menus are navigation sets filling slots. A slot is a place on the site’s pages that the project declares in site.yaml, such as the header or the footer. A set is a tree of items that staff edit in the admin’s Navigation screen and assign to a slot. Templates read the assigned set as nav.<slot>.
Items point at documents, files, a type’s index page or an address. A document item links to the document’s live address in the visitor’s locale and drops out of the menu by itself when the document is unpublished, so a menu never links to a page that is gone.
Add a menu
Declare the slot in site.yaml:
# config/sites/<site>/site.yaml
nav:
main: { label: { en: Header }, depth: 2 }
footer: { label: { en: Footer }, depth: 1 } Run engine-dmo check --fix, which writes each slot’s id. The id is what the stored sets are keyed by, so renaming a slot keeps its sets.
Render it in the layout:
<nav aria-label="Main">
<ul>{% nav item in nav.main %}<li><a href="{{ item.url|url }}"{% if item.isCurrent %} aria-current="page"{% endif %}>{{ item.label }}</a>{% ifchildren %}<ul>{% children %}</ul>{% endifchildren %}</li>{% endnav %}</ul>
</nav> Create the set. Either in the admin (Navigation, choose the site and slot, add items, assign), or as a seed file that engine-dmo seed apply loads:
# seed/navigation/main.yaml
site: main
slot: main
name: Header
assigned: true
items:
- label: { en: Visit }
document: page/visit
- label: { en: Search }
url: /search
- label: { en: Find us }
children:
- label: { en: Getting here }
document: page/visit
suffix: "#getting-here"
- label: { en: Box office }
url: "https://tickets.example.org/" engine-dmo seed verify checks the file against the site’s slots and the seed’s documents, and names the file in each problem (seed/navigation/main.yaml: document page/visit is not in the seed). Its summary line counts documents, assets and users only, never navigation sets, so a clean run that says nothing about navigation means every navigation file passed. To see the menu itself, engine-dmo seed apply against a database and engine-dmo render /.
On a site with more than one locale, give every item’s label a value in each locale ({ en: Visit, cy: Ymweld }). A missing one falls back along the locale chain to the source locale’s label without a warning.
A slot with no assigned set renders as an empty list, so a template can always loop over it.
Slots
Each key under nav: is a slot. The key is how templates name it (nav.main), so it is letters, digits and underscores, starting with a letter.
| Key | Default | Meaning |
|---|---|---|
id | written by check --fix | The slot’s stable id. |
label | the key | The slot’s name in the admin, per locale. |
depth | 1 | The deepest an item may sit, 1 to 5. 1 is a flat list; 2 allows one level of children. Items deeper than the depth are not rendered. |
A slot takes no other keys. Older projects that listed items directly under nav.<slot> are refused with a hint to move the items into a set.
Items
Every item has a label per locale and at most one target:
| Seed key | Admin kind | Links to | Drops out when |
|---|---|---|---|
document | Document | The document’s live address in the visitor’s locale. In a seed, a type/key reference to a seeded document. | The document is not published on the site in the locale chain. |
asset | File | A library file’s address. | The asset is deleted. |
index | Index page | A type’s index page, from its route’s index. The label defaults to the type’s plural name. | The type has no index route on the site. |
url | Address | A path on the site (/search, prefixed with the locale as path() does) or an absolute http or https address with a host. | The address is not a valid link. |
| none | Heading | Nothing: a heading that groups its children, such as a footer column. | It has no children left to show. |
suffix adds a fixed ending to the resolved address: an anchor (#getting-here) or a query string (?category=stay).
label is read along the visitor’s locale chain. An item with no label in the chain takes its target’s title (a document’s title, a type’s plural name, an external host name); an item with no label at all is not rendered.
In templates
nav holds every slot of the site, each a list of items:
| Key | Meaning |
|---|---|
label | The label in the visitor’s locale. |
url | The address. Print it through the url filter. |
kind | document, asset, index, url or heading. |
isExternal | True for an address on another host. |
isCurrent | True when the item’s address is the page being served (the same path, trailing slash ignored, and the same values for any query parameters the item names). |
isActive | True when the item or any item beneath it is current: the active trail. |
depth | 1 for the top level. |
children | The items beneath it. |
id, type | A document item’s document id and type key. |
The {% nav %} tag renders a tree: its body runs once per item with the item bound to the name you give, {% ifchildren %} renders only when the item has children, and {% children %} renders the same body for them one level deeper. It works over any list of maps with children, not only nav. A flat list needs only a for loop:
<ul>{% for item in nav.footer %}<li><a href="{{ item.url|url }}">{{ item.label }}</a></li>{% endfor %}</ul> A heading item has an empty url; render it as text:
{% nav item in nav.footer %}
{% if item.kind == "heading" %}<h2>{{ item.label }}</h2>{% else %}<a href="{{ item.url|url }}">{{ item.label }}</a>{% endif %}
{% ifchildren %}<ul>{% children %}</ul>{% endifchildren %}
{% endnav %} Navigation is resolved once per site and locale and cached; publishing a document a menu names, or editing a set, refreshes it. A {% cache %} block that names nav is invalidated with it.
Other navigation
| Need | Use |
|---|---|
| Breadcrumbs on a nested page | entry.ancestors, or ancestors(doc): the published ancestors on the site, root first. |
| A page’s sub-pages | entry.children, or children(doc): the published children, in title order. |
| A table of contents for rich text | The toc filter on a Portable Text value: a nav of its headings, linking to their ids. |
| A language switcher | localeSwitcher(): a nav of the locales the page exists in, the current one marked. localeUrl(code) is the current page in another locale, empty when it has no translation. |
| Links to another site of the install | siteUrl(path, site="other"); sites lists every site when there is more than one. |
| Pagination on index and search pages | pagination (Search). |
{% if entry.ancestors %}
<nav aria-label="Breadcrumb"><ol>
{% for a in entry.ancestors %}<li><a href="{{ a.uri|url }}">{{ a.title }}</a></li>{% endfor %}
<li aria-current="page">{{ entry.title }}</li>
</ol></nav>
{% endif %} Breadcrumbs and children apply to types whose routes nest; see Routes.
The admin
The Navigation screen lists the slots a site declares, the sets that fill them and which set is assigned. A set’s items are edited with plain forms: add an item of a kind, set its labels per locale, move it up, down, in (under the item above) or out, remove it. Sets can be renamed, deleted and assigned to their slot. Every change works from the keyboard and without JavaScript. Sets are kept per slot, so a new menu can be prepared and assigned when it is ready.
Every route of the screen needs nav.manage on the site, which the built-in Administrator and Editor roles hold.
Search
How to give a site a search page, connect Meilisearch, Elasticsearch or OpenSearch, what is indexed, the search.html template and search() function, browse mode without a backend, synonyms and the zero-result report.
Forms and submissions
How to build a contact or enquiry form with no code: declaring an endpoint in endpoints.yaml, its inputs, validation, spam protection and rate limits, the store and mail steps, retention, the drawn form, the response templates and the submissions inbox.