EmDash menus are ordered lists of links that you manage through the admin interface. Menus support nesting for dropdowns and can link to pages, posts, taxonomy terms, or external URLs.
Querying Menus
Use getMenu() to fetch a menu by its unique name:
---
import { getMenu } from "emdash";
const primaryMenu = await getMenu("primary");
---
{primaryMenu && (
<nav>
<ul>
{primaryMenu.items.map(item => (
<li>
<a href={item.url}>{item.label}</a>
</li>
))}
</ul>
</nav>
)}
The function returns null if no menu exists with that name.
Menu Structure
A menu contains metadata and an array of items:
interface Menu {
id: string;
name: string; // Unique identifier ("primary", "footer")
label: string; // Display name ("Primary Navigation")
items: MenuItem[];
}
interface MenuItem {
id: string;
label: string;
url: string; // Resolved URL
target?: string; // "_blank" for new window
titleAttr?: string; // HTML title attribute
cssClasses?: string; // Custom CSS classes
children: MenuItem[]; // Nested items for dropdowns
}
URLs are resolved automatically based on the item type:
- Page/Post items resolve to
/{collection}/{slug} - Taxonomy items resolve to
/{taxonomy}/{slug} - Collection items resolve to
/{collection}/ - Custom links use the URL as-is
Rendering Nested Menus
Menu items can have children for dropdown navigation. Handle nesting by recursively rendering the children array:
---
import { getMenu } from "emdash";
import type { MenuItem } from "emdash";
interface Props {
name: string;
}
const menu = await getMenu(Astro.props.name);
---
{menu && (
<nav class="nav">
<ul class="nav-list">
{menu.items.map(item => (
<li class:list={["nav-item", item.cssClasses]}>
<a
href={item.url}
target={item.target}
title={item.titleAttr}
aria-current={Astro.url.pathname === item.url ? "page" : undefined}
>
{item.label}
</a>
{item.children.length > 0 && (
<ul class="submenu">
{item.children.map(child => (
<li>
<a href={child.url} target={child.target}>
{child.label}
</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}
Menu Item Types
The admin supports five types of menu items:
| Type | Description | URL Resolution |
|---|---|---|
page | Link to a page | /{collection}/{slug} |
post | Link to a post | /{collection}/{slug} |
taxonomy | Link to a category or tag | /{taxonomy}/{slug} |
collection | Link to a collection archive | /{collection}/ |
custom | External or custom URL | Used as-is |
Listing All Menus
Use getMenus() to retrieve all menu definitions (without items):
import { getMenus } from "emdash";
const menus = await getMenus();
// Returns: [{ id, name, label }, ...]
This is primarily useful for admin interfaces or debugging.
Creating Menus
Create menus through the admin interface at /_emdash/admin/menus, or use the admin API:
POST /_emdash/api/menus
Content-Type: application/json
{
"name": "footer",
"label": "Footer Navigation"
}
Add items to a menu:
POST /_emdash/api/menus/footer/items
Content-Type: application/json
{
"type": "page",
"referenceCollection": "pages",
"referenceId": "page_privacy",
"label": "Privacy Policy"
}
Add a custom external link:
POST /_emdash/api/menus/footer/items
Content-Type: application/json
{
"type": "custom",
"customUrl": "https://github.com/example",
"label": "GitHub",
"target": "_blank"
}
Reordering and Nesting
Update item order and parent-child relationships with the reorder endpoint:
POST /_emdash/api/menus/primary/reorder
Content-Type: application/json
{
"items": [
{ "id": "item_1", "parentId": null, "sortOrder": 0 },
{ "id": "item_2", "parentId": null, "sortOrder": 1 },
{ "id": "item_3", "parentId": "item_2", "sortOrder": 0 }
]
}
This makes item_3 a child of item_2, creating a dropdown.
Complete Example
The following example shows a responsive header with primary navigation:
---
import { getMenu, getSiteSettings } from "emdash";
const settings = await getSiteSettings();
const primaryMenu = await getMenu("primary");
---
<html lang="en">
<head>
<title>{settings.title}</title>
</head>
<body>
<header class="header">
<a href="/" class="logo">
{settings.logo ? (
<img src={settings.logo.url} alt={settings.logo.alt || settings.title} />
) : (
settings.title
)}
</a>
{primaryMenu && (
<nav class="main-nav" aria-label="Main navigation">
<ul>
{primaryMenu.items.map(item => (
<li class:list={[item.cssClasses, { "has-children": item.children.length > 0 }]}>
<a
href={item.url}
target={item.target}
aria-current={Astro.url.pathname === item.url ? "page" : undefined}
>
{item.label}
</a>
{item.children.length > 0 && (
<ul class="dropdown">
{item.children.map(child => (
<li>
<a href={child.url} target={child.target}>{child.label}</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}
</header>
<main>
<slot />
</main>
</body>
</html>
API Reference
getMenu(name)
Fetch a menu by name with all items and resolved URLs.
Parameters:
name— The menu’s unique identifier (string)
Returns: Promise<Menu | null>
getMenus()
List all menu definitions without items.
Returns: Promise<Array<{ id: string; name: string; label: string }>>