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.
-
Click Create Menu and enter a name and label. Templates query the name, while the label identifies the menu in the admin.
-
Click Add Content to link an entry, or Add Custom Link to enter an external URL or a root-relative site path.
-
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.
How menu links resolve
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 kind | URL returned to the template |
|---|---|
| Content entry | The 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 link | The 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.