Meridian handbook
Features

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.

KeyDefaultMeaning
idwritten by check --fixThe slot’s stable id.
labelthe keyThe slot’s name in the admin, per locale.
depth1The 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 keyAdmin kindLinks toDrops out when
documentDocumentThe 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.
assetFileA library file’s address.The asset is deleted.
indexIndex pageA 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.
urlAddressA 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.
noneHeadingNothing: 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:

KeyMeaning
labelThe label in the visitor’s locale.
urlThe address. Print it through the url filter.
kinddocument, asset, index, url or heading.
isExternalTrue for an address on another host.
isCurrentTrue 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).
isActiveTrue when the item or any item beneath it is current: the active trail.
depth1 for the top level.
childrenThe items beneath it.
id, typeA 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

NeedUse
Breadcrumbs on a nested pageentry.ancestors, or ancestors(doc): the published ancestors on the site, root first.
A page’s sub-pagesentry.children, or children(doc): the published children, in title order.
A table of contents for rich textThe toc filter on a Portable Text value: a nav of its headings, linking to their ids.
A language switcherlocaleSwitcher(): 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 installsiteUrl(path, site="other"); sites lists every site when there is more than one.
Pagination on index and search pagespagination (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.