Navigation Menus

On this page

Menus let editors manage ordered links without changing a site template. A menu has a stable name, such as primary or footer, and a separate set of items for each translated menu.

Create a menu

Create and arrange menus in Menus in the EmDash admin.

  1. Click Create Menu and enter a name and label. Templates query the name, while the label identifies the menu in the admin.

  2. Click Add Content to link an entry, or Add Custom Link to enter an external URL or a root-relative site path.

  3. Use Move up and Move down to set the order. Edit an item and choose a Parent to nest it.

Use the same menu name in every locale. On a multilingual site, open a menu and use its Translations panel to create and edit the other locale versions. EmDash keeps the translations of the same content or taxonomy term connected, so getMenu() can use the label and slug for the requested locale.

Content and taxonomy menu items store a reference rather than a finished URL. When a template calls getMenu(), EmDash resolves that reference using the current collection and locale data.

Menu item kindURL returned to the template
Content entryThe collection’s urlPattern, or /{collection}/{slug} when the collection has no pattern
Taxonomy term/{taxonomy}/{slug} using the resolved term translation
Collection archive/{collection}/
Custom linkThe external URL or root-relative path entered by the editor

Each returned item also contains its label, optional target, title attribute, CSS classes, and nested children. The rendering example below uses those values directly.

Render a menu

Call getMenu() in a server-rendered Astro component. It returns null when the name does not exist.

The following layout renders a primary menu and one level of nested items:

---
import { getMenu } from "emdash";
import { getRelativeLocaleUrl } from "astro:i18n";

const locale = Astro.currentLocale;
const menu = await getMenu("primary", { locale });

function menuHref(url: string) {
  return locale && url.startsWith("/")
    ? getRelativeLocaleUrl(locale, url)
    : url;
}
---

{menu && menu.items.length > 0 && (
  <nav aria-label="Primary navigation">
    <ul>
      {menu.items.map((item) => {
        const href = menuHref(item.url);

        return (
          <li class:list={item.cssClasses}>
            <a
              href={href}
              target={item.target}
              rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
              title={item.titleAttr}
              aria-current={Astro.url.pathname === href ? "page" : undefined}
            >
              {item.label}
            </a>

            {item.children.length > 0 && (
              <ul>
                {item.children.map((child) => {
                  const childHref = menuHref(child.url);

                  return (
                    <li class:list={child.cssClasses}>
                      <a
                        href={childHref}
                        target={child.target}
                        rel={child.target === "_blank" ? "noopener noreferrer" : undefined}
                        title={child.titleAttr}
                        aria-current={Astro.url.pathname === childHref ? "page" : undefined}
                      >
                        {child.label}
                      </a>
                    </li>
                  );
                })}
              </ul>
            )}
          </li>
        );
      })}
    </ul>
  </nav>
)}

getMenu() chooses the explicit locale first, then the current request locale, then the configured default locale. If a menu or referenced entry is missing in that locale, lookup follows the configured fallback chain.

Menu item URLs contain the collection URL pattern or taxonomy path, but they do not contain Astro’s locale prefix. The menuHref() helper adds that prefix to root-relative links and leaves external links unchanged.

The example renders one child level, which covers a typical dropdown. If editors can create deeper navigation, move the item markup into a recursive component that renders each item’s children with the same rules.

Use menus in widget areas

A menu widget places an existing menu inside a widget area. The widget reads the menu for the current request locale. Use the direct rendering pattern above when the site needs custom nested markup or explicit locale URL handling.

Query menu data directly

Use getMenus() when a template needs the available menu definitions rather than one menu’s items. The runtime API reference documents the query signatures and return values.

For programmatic menu changes, authenticate with a Bearer token and add X-EmDash-Request: 1 to every state-changing request. See the menu endpoints for request bodies and responses.

See Internationalization for locale routing and fallback configuration.