Navigation
Madori provides a navigation management system for building and maintaining site menus, sidebars, and any other link structure. Navigations are stored as flat YAML files, edited visually in the Control Panel with drag-and-drop tree editing, and queried from your frontend via the REST API or GraphQL.
Each navigation is identified by a handle (e.g. main, footer, docs) and contains a nested tree of items. Items can be URLs, references to collection entries, or text-only labels used as group headings.
Configuration Reference
Navigation Definition
Navigation definitions live at resources/definitions/navigations/{handle}.yaml (or .json). They configure the behaviour of a navigation in the Control Panel editor.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
title |
string |
Yes | — | Display name shown in the CP sidebar and editor |
max_depth |
number |
No | unlimited | Maximum nesting depth allowed. A flat list has depth 0; each level of nesting adds 1 |
collections |
string[] |
No | all | Which collections are available when adding entry reference items |
Example definition:
# resources/definitions/navigations/main.yaml
title: Main Navigation
max_depth: 3
collections:
- pages
- blog
Navigation Data
Navigation content is stored at content/navigation/{handle}.yaml. This is the file the editor interface reads and writes.
| Property | Type | Description |
|---|---|---|
items |
NavigationItem[] |
Top-level array of navigation items |
Item Properties
Each item in the navigation tree supports these properties:
| Property | Type | Required | Description |
|---|---|---|---|
label |
string |
Yes | Display text for the navigation item |
url |
string |
No | Link URL (absolute or relative path) |
entry |
string |
No | Reference to a collection entry by slug (e.g. pages/about) |
external |
boolean |
No | When true, link opens in a new tab |
children |
NavigationItem[] |
No | Nested child items |
An item should have either a url or an entry — not both. Items with neither serve as text-only group headings in menus.
Creating a Navigation
Via the Control Panel
- Navigate to Navigation in the CP sidebar
- Click Create Navigation
- Enter a handle (e.g.
footer) and title (e.g. "Footer Navigation") - Optionally set a
max_depthto limit nesting - Save the definition
The editor interface appears immediately, ready for you to add items.
Via File
Create both the definition and data files:
# resources/definitions/navigations/footer.yaml
title: Footer Navigation
max_depth: 2
collections:
- pages
# content/navigation/footer.yaml
items:
- label: Home
url: /
- label: About
url: /about
- label: Contact
url: /contact
Editing in the Control Panel
The navigation editor provides a visual tree interface for managing items.
Adding Items
Click Add Item to insert a new navigation item. Choose the item type:
| Type | Use Case |
|---|---|
| URL | Links to any URL (internal path or external) |
| Entry Reference | Links to a collection entry — URL resolves automatically |
| Text | Non-linking label used as a group heading |
Nested Editing
Drag items to reorder them within the same level or nest them under a parent item. The editor supports:
- Reordering: Drag an item up or down within its current level
- Nesting: Drag an item onto another item to make it a child
- Outdenting: Drag a nested item to the left to promote it up one level
- Keyboard reordering: Use the up/down arrow controls for accessible reordering without drag-and-drop
Max Depth Enforcement
When a navigation definition specifies max_depth, the editor prevents nesting beyond that limit:
- Drop targets disappear when dropping would exceed the depth limit
- Visual feedback indicates when the maximum depth is reached
- The API rejects save operations that would violate the constraint, returning a
DEPTH_EXCEEDEDerror
A max_depth of 0 means a flat list only (no nesting). A max_depth of 1 allows one level of children, and so on.
Removing Items
When you remove an item that has children, the editor prompts with two options:
- Delete children: Removes the item and all its descendants
- Promote children: Removes the item but moves its children up to the parent level
Inline Editing
Each item displays its label and link target inline. Click the label or URL to edit them directly in the tree without opening a separate form.
Item Types
URL Items
Link to any path or external URL:
- label: Blog
url: /blog
- label: GitHub
url: https://github.com/my-org/my-repo
external: true
Set external: true to indicate the link should open in a new tab. Your frontend template can use this to render a target attribute or external link icon.
Entry Reference Items
Reference a collection entry by its slug. The URL resolves automatically based on the entry's collection routing:
- label: Getting Started
entry: pages/getting-started
- label: Latest Post
entry: blog/hello-world
Entry references are useful because the link stays valid even if the entry's URL structure changes.
Text Items
Items with only a label (no url or entry) serve as non-clickable headings:
- label: Resources
children:
- label: Documentation
url: /docs
- label: API Reference
url: /api
Text items are typically used as parent groups in dropdown menus or sidebar sections.
Frontend Rendering
REST API
Fetch navigation data from the REST API:
GET /api/navigation # List all navigations
GET /api/navigation/{handle} # Get a single navigation tree
Response format:
{
"data": {
"handle": "main",
"items": [
{ "label": "Home", "url": "/" },
{
"label": "Documentation",
"url": "/docs",
"children": [
{ "label": "Getting Started", "entry": "pages/getting-started" },
{ "label": "Configuration", "url": "/docs/configuration" }
]
},
{ "label": "GitHub", "url": "https://github.com/example", "external": true }
]
}
}
GraphQL
Query navigation data through the GraphQL API:
{
navigation(handle: "main") {
handle
items {
label
url
entry
external
children {
label
url
entry
external
children {
label
url
}
}
}
}
}
List all navigations:
{
navigations {
handle
items {
label
url
}
}
}
Save API
Update a navigation tree programmatically:
PUT /api/navigation/{handle}
Content-Type: application/json
{
"items": [
{ "label": "Home", "url": "/" },
{ "label": "About", "url": "/about" }
]
}
If the tree violates the configured max_depth, the API returns:
{
"error": {
"code": "DEPTH_EXCEEDED",
"message": "Navigation tree exceeds maximum allowed depth",
"maxDepth": 2,
"actualDepth": 3
}
}
Usage Examples
Rendering a Navigation in Next.js
Fetch the navigation at build time or request time and render it recursively:
import Link from 'next/link'
interface NavItem {
label: string
url?: string
entry?: string
external?: boolean
children?: NavItem[]
}
async function getNavigation(handle: string): Promise<NavItem[]> {
const res = await fetch(`${process.env.SITE_URL}/api/navigation/${handle}`)
const json = await res.json()
return json.data.items
}
function NavLink({ item }: { item: NavItem }) {
const href = item.url ?? `/${item.entry}`
if (!item.url && !item.entry) {
return <span className="font-semibold">{item.label}</span>
}
if (item.external) {
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{item.label}
</a>
)
}
return <Link href={href}>{item.label}</Link>
}
function NavTree({ items }: { items: NavItem[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.label}>
<NavLink item={item} />
{item.children && <NavTree items={item.children} />}
</li>
))}
</ul>
)
}
export default async function MainNav() {
const items = await getNavigation('main')
return (
<nav aria-label="Main navigation">
<NavTree items={items} />
</nav>
)
}
Fetching Navigation via GraphQL
Using a GraphQL client like graphql-request:
import { gql, request } from 'graphql-request'
const NAVIGATION_QUERY = gql`
query GetNavigation($handle: String!) {
navigation(handle: $handle) {
items {
label
url
entry
external
children {
label
url
entry
external
}
}
}
}
`
const data = await request('/api/graphql', NAVIGATION_QUERY, { handle: 'main' })
Sidebar Navigation with Active State
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
interface NavItem {
label: string
url?: string
children?: NavItem[]
}
function SidebarNav({ items }: { items: NavItem[] }) {
const pathname = usePathname()
return (
<nav aria-label="Documentation sidebar">
<ul className="space-y-1">
{items.map((item) => {
const isActive = pathname === item.url
return (
<li key={item.label}>
{item.url ? (
<Link
href={item.url}
className={isActive ? 'font-bold text-blue-600' : 'text-gray-700'}
aria-current={isActive ? 'page' : undefined}
>
{item.label}
</Link>
) : (
<span className="text-sm font-semibold uppercase text-gray-500">
{item.label}
</span>
)}
{item.children && (
<ul className="ml-4 mt-1 space-y-1">
{item.children.map((child) => (
<li key={child.label}>
<Link
href={child.url ?? '#'}
className={pathname === child.url ? 'font-bold' : ''}
>
{child.label}
</Link>
</li>
))}
</ul>
)}
</li>
)
})}
</ul>
</nav>
)
}
Common Patterns
Multi-Level Dropdown Menu
Structure a navigation with grouped sections for a mega-menu or multi-level dropdown:
# content/navigation/main.yaml
items:
- label: Home
url: /
- label: Products
children:
- label: Software
children:
- label: CMS
url: /products/cms
- label: Analytics
url: /products/analytics
- label: Services
children:
- label: Consulting
url: /services/consulting
- label: Training
url: /services/training
- label: Blog
url: /blog
- label: Contact
url: /contact
Set max_depth: 2 on the definition to allow this structure but prevent deeper nesting.
Footer with Column Groups
Use text-only items as column headings in a footer layout:
# content/navigation/footer.yaml
items:
- label: Product
children:
- label: Features
url: /features
- label: Pricing
url: /pricing
- label: Changelog
url: /changelog
- label: Company
children:
- label: About
url: /about
- label: Careers
url: /careers
- label: Blog
url: /blog
- label: Legal
children:
- label: Privacy Policy
url: /privacy
- label: Terms of Service
url: /terms
Your frontend template renders top-level items as column headings and children as the links within each column.
Documentation Sidebar
A flat navigation for ordered documentation pages:
# resources/definitions/navigations/docs.yaml
title: Documentation Sidebar
max_depth: 1
collections:
- pages
# content/navigation/docs.yaml
items:
- label: Getting Started
entry: pages/docs/getting-started
- label: Configuration
entry: pages/docs/configuration
- label: Collections
entry: pages/docs/collections
- label: Blueprints
entry: pages/docs/blueprints
- label: Field Types
entry: pages/docs/field-types
- label: Advanced
children:
- label: GraphQL API
entry: pages/docs/graphql
- label: CLI
entry: pages/docs/cli
- label: Deployment
entry: pages/docs/deployment
Setting max_depth: 1 keeps the sidebar manageable — one level of grouping, no deeper nesting.
Mixing Internal and External Links
Combine internal paths, entry references, and external links in a single navigation:
items:
- label: Home
url: /
- label: Documentation
entry: pages/docs/getting-started
- label: API Reference
url: /api/graphql
- label: GitHub
url: https://github.com/my-org/my-project
external: true
- label: Discord
url: https://discord.gg/invite-code
external: true
Programmatic Navigation Updates
Update navigation trees from build scripts or external integrations:
async function addNavItem(handle: string, item: { label: string; url: string }) {
// Fetch current tree
const res = await fetch(`http://localhost:3000/api/navigation/${handle}`)
const { data } = await res.json()
// Append new item
data.items.push(item)
// Save updated tree
await fetch(`http://localhost:3000/api/navigation/${handle}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: data.items }),
})
}
Managing Navigations
Control Panel
Navigate to Navigation in the CP sidebar to:
- Create new navigation definitions
- Edit tree structure with drag-and-drop
- Add, remove, and reorder items
- Configure max depth and available collections
API
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/navigation |
List all navigations |
| GET | /api/navigation/{handle} |
Get a navigation tree |
| PUT | /api/navigation/{handle} |
Save/update a navigation tree |
File-Based Management
Edit navigation YAML directly for version control:
content/navigation/
├── main.yaml
├── footer.yaml
└── docs.yaml
resources/definitions/navigations/
├── main.yaml
├── footer.yaml
└── docs.yaml
Changes to navigation files are reflected immediately on the next API request or page load.