---
title: Navigation Menus
description: Create and manage navigation menus for headers, footers, and sidebars.
---

import { Aside, Steps, Tabs, TabItem, Code } from "@astrojs/starlight/components";

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:

```astro title="src/layouts/Base.astro"
---
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:

```ts
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:

```astro title="src/components/Navigation.astro"
---
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>
)}
```

<Aside>
	Use `aria-current="page"` to indicate the current page in navigation. Screen readers announce
	this, and the `[aria-current="page"]` CSS selector enables styling the active link.
</Aside>

## 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):

```ts
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:

```http
POST /_emdash/api/menus
Content-Type: application/json

{
  "name": "footer",
  "label": "Footer Navigation"
}
```

Add items to a menu:

```http
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:

```http
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:

```http
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:

```astro title="src/layouts/Base.astro"
---
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 }>>`
