How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Output a menu with the current item highlighted

Read the admin's menu by its exact name, loop the root elements and their children, and mark the current item with _url.isCurrentPath() or a path prefix. Unpublished elements show to admins only.

Context

Context. Navigation belongs to editors, so it comes from an admin menu, not from links in a template. _menu->get() loads a menu by its exact name; renaming it in the admin empties your navigation, so always check for nothing. Each element has a URL, a name and children. To highlight the current item, ask _url->isCurrentPath() for an exact match, and compare the path prefix so a section stays highlighted on its child pages. Menu elements are publishable: an unpublished one is hidden from visitors but visible to a signed-in admin, which explains "the menu has an item I cannot see".

Steps

  1. Load the menu and stop if it is missing.
  2. For each root element, compute active: an exact match, or the element's URL is a prefix of the current path (excluding /, which prefixes everything).
  3. Print the item with a class and aria-current when active, and its children as a submenu.

Template code

Template code
{$menu = $_menu->get('Header Menu')}
{if $menu}
<nav aria-label="Main"><ul>
{foreach $menu->getRootElements() as $item}
  {$url = $item->getUrl()}
  {$active = $_url->isCurrentPath($url) || ($url != '/' && strpos($_request->getPath(), $url) === 0)}
  <li class="{if $active}is-active{/if}">
    <a href="{$url}"{if $active} aria-current="page"{/if}>{$item->getName()|escape}</a>
    {if $item->getChildren()->count() > 0}
    <ul>{foreach $item->getChildren() as $child}<li><a href="{$child->getUrl()}">{$child->getName()|escape}</a></li>{/foreach}</ul>
    {/if}
  </li>
{/foreach}
</ul></nav>
{/if}
{% set menu = _menu.get('Header Menu') %}
{% if menu %}
<nav aria-label="Main"><ul>
{% for item in menu.getRootElements() %}
  {% set url = item.getUrl() %}
  {% set active = _url.isCurrentPath(url) or (url != '/' and _request.getPath() starts with url) %}
  <li class="{% if active %}is-active{% endif %}">
    <a href="{{ url }}"{% if active %} aria-current="page"{% endif %}>{{ item.getName()|e }}</a>
    {% if item.getChildren()|length > 0 %}
    <ul>{% for child in item.getChildren() %}<li><a href="{{ child.getUrl() }}">{{ child.getName()|e }}</a></li>{% endfor %}</ul>
    {% endif %}
  </li>
{% endfor %}
</ul></nav>
{% endif %}

Why this and not that

Not links written into the template: editors reorder and rename navigation weekly, and a menu edit changes the site without a deploy. Not comparing names: two elements can share a name, and names are translated. The prefix match is deliberately loose (/about also matches /about-us); when that matters, compare with a trailing slash added to both sides.

Go deeper

Was this page helpful?

Your answer goes to the docs team.