Meridian handbook
For agentsRecipes

Navigation menu

How an agent adds header and footer menus to a Meridian site with navigation slots in site.yaml, a seeded navigation set and the nav tag in the layout, marking the current page and hiding an empty menu.

Menus are navigation sets that fill slots. The project declares the slots; editors fill them in the admin’s Navigation screen; you seed the first version so the site works from day one. A menu item that points at a document drops out by itself when the document is unpublished.

flowchart LR
  slot["site.yaml nav: main<br/>(the slot)"] --> set["seed/navigation/main.yaml<br/>(the set, assigned)"]
  set --> admin["Editors change it<br/>in the admin"]
  set --> tag["base.html<br/>nav tag on nav.main"]
  admin --> tag

Declare the slots

# config/sites/main/site.yaml
nav:
  main: { label: { en: Header }, depth: 2 }
  footer: { label: { en: Footer }, depth: 1 }
engine-dmo check --fix

check refuses a slot without an id (nav slot "main" has no id); check --fix writes nav_ ids. depth is how deep items may nest, 1 to 5.

Seed the menus

# seed/navigation/main.yaml
site: main
slot: main
name: Header
assigned: true
items:
  - label: { en: Walks }
    index: walk
  - label: { en: Contact }
    document: page/contact
# seed/navigation/footer.yaml
site: main
slot: footer
name: Footer
assigned: true
items:
  - label: { en: Accessibility }
    document: page/accessibility
  - label: { en: Search }
    url: /search
Item keyLinks to
document: <type>/<seed key>A seeded document, in the visitor’s locale
index: <type>The type’s index page
url: /path or url: https://...A path on the site (locale-prefixed) or an external address
asset: <seed key>A library file
none, with children:A heading grouping its children

Each item may add suffix: "#anchor" and children: (up to the slot’s depth). Every document an item names must exist in the seed.

engine-dmo seed verify
engine-dmo seed reset --confirm
engine-dmo seed apply

seed verify loads the navigation files with the rest and fails on an item that names a document not in the seed (seed/navigation/main.yaml: document page/nosuch is not in the seed), or a slot site.yaml does not declare. Its summary line counts documents and assets only, so a menu that loads is silent there. Only seed apply counts the menus: it reports navigation: 2 sets, 4 items. Without a database, a clean seed verify is the evidence that the menus will load.

On a multilingual site give each item’s label every locale (label: { en: Walks, cy: Teithiau }); check does not warn about a missing one, and the item shows its source-locale label.

Render the menus in the layout

{# in templates/_shared/base.html #}
<nav aria-label="{{ t('nav.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>
...
<footer>
  {% if nav.footer %}<nav aria-label="{{ t('nav.footer') }}">
    <ul>{% for item in nav.footer %}<li><a href="{{ item.url|url }}">{{ item.label }}</a></li>{% endfor %}</ul>
  </nav>{% endif %}
</footer>
# config/strings/en.yaml
strings:
  nav.main: Main
  nav.footer: Footer
  • {% nav %} renders a tree: {% ifchildren %} renders only when the item has children and {% children %} repeats the body one level down. A flat menu needs only {% for %}.
  • Mark the current page with aria-current="page" from item.isCurrent; item.isActive is true on the trail above it.
  • Wrap a menu that may be empty in {% if nav.<slot> %} so the page has no empty landmark.
  • A heading item (item.kind == "heading") has an empty url: print it as text.

Verify

engine-dmo render /walks --json

The html holds both menus, the Walks item carries aria-current="page", and a11y is empty. See Navigation for every item property and breadcrumbs.