Adds a menus collection to Payload v3 for modelling navigation, including simple links, nested dropdowns and mega menus. Menus are fetched by slug and locale through a REST endpoint, with helper functions for server and client code. Configured via the plugin options for reference collections, nesting depth, cache TTL and dynamic sources. Requires Payload 3.37 or later.
A plugin for Payload CMS that provides a flexible and customisable menu system.
Installation
npm i @foundrykit/menu-plugin6
Stars
0
Forks
1
Open Issues
3 months ago
Last Updated
0
Weekly Downloads
v0.5.0
npm Version
200.1 KB
Unpacked Size
0
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1155793773-root)<a href="https://payloaddirectory.dev/plugins/1155793773-root"><img src="https://payloaddirectory.dev/badge/1155793773-root.svg" alt="Payload Directory"></a>README
A Payload CMS v3 plugin that adds a configurable menu management system to the admin panel. It lets you model nested navigation, fetch menus by slug, and consume them from regular frontend code without depending on Payload admin UI context.
reveal layout) or classic
multi-column layout (columns), each with a featured panelafterRead hookGET /api/menus/:slug endpoint with locale and depth supportfetchMenu helper plus a useMenu hook built on top of itslug + localemegaColumns data to the new megaEntries
shapepnpm add @foundrykit/menu-plugin
import { buildConfig } from 'payload'
import { menuPlugin } from '@foundrykit/menu-plugin'
export default buildConfig({
plugins: [
menuPlugin({
baseUrl: 'https://example.com',
relationTo: ['pages', 'posts'],
}),
],
})
This registers a menus collection in Payload.
| Option | Type | Default | Description |
|---|---|---|---|
baseUrl | string | '' | Base URL prefix shown for internal links in the admin |
cacheTTL | number | 60000 | Server cache TTL in milliseconds |
childrenDepth | number | 1 | Relationship depth used when resolving dynamic mega entry children |
disabled | boolean | false | Keep the collection schema, but disable plugin runtime endpoints, hooks, and admin helper UI |
dynamicSources | DynamicSource[] | [] | Sources used to auto-populate dynamic mega entries from related collections (see below) |
enableImportExport | boolean | true | Register the import/export endpoints |
maxDepth | number | 1 | Maximum nesting depth for dropdown items |
mediaCollection | string | undefined | Upload collection used for mega menu featured images |
relationTo | string | string[] | 'pages' | Collection(s) available for reference links |
requireAdminForImportExport | boolean | true | Require an authenticated admin user for import/export endpoints |
Example:
menuPlugin({
baseUrl: 'https://mysite.com',
cacheTTL: 300_000,
maxDepth: 3,
relationTo: ['pages', 'posts', 'products'],
})
Menus are uniquely identified by slug + locale.
slug: "main-menu", locale: "en" and slug: "main-menu", locale: "fr" can coexistEach menu item has a kind (itemType): link, dropdown, or mega. A mega item
exposes:
megaLayout: reveal (default — a flat list of rows that reveal their
children in a second panel) or columns (classic multi-column layout)defaultFeatured: a fallback featured panel (image, heading, description, CTA)megaEntries: a flat list of entries. Each entry is itself a link and can
reveal children in a secondary panel, plus an optional per-entry featured
panel whose mode is inherit (use defaultFeatured), manual, or dynamic.Featured images use the mediaCollection option as their upload relationTo.
An entry's source can be manual (children typed by hand) or dynamic
(children pulled from a related collection). Dynamic entries are resolved
server-side by an afterRead hook on the menus collection, so they work
through every read path — the /api/menus/:slug endpoint, payload.find, and
menus populated as a relationship on another global/collection — with no extra
wiring. Resolution is read-only, idempotent, locale-aware, and degrades to empty
children on misconfiguration (never throws).
Configure the available sources via dynamicSources:
import { menuPlugin, type DynamicSource } from '@foundrykit/menu-plugin'
const destinations: DynamicSource = {
name: 'destinations',
label: 'Destinations',
collection: 'destinations', // queried for children
parentCollection: 'regions', // editor picks a parent from here
parentField: 'region', // field on a child relating it to its parent
labelField: 'title', // child field used as the label
hrefBuilder: (doc) => `/destinations/${doc.slug as string}`,
sort: 'title',
limit: 0, // optional; defaults to 0 (return all)
featured: {
imageField: 'heroImage', // dot-path to an upload/media field
headingField: 'title',
descriptionField: 'excerpt',
ctaHrefBuilder: (doc) => `/destinations/${doc.slug as string}`,
ctaLabel: 'Explore',
},
}
menuPlugin({
relationTo: ['pages'],
mediaCollection: 'media',
dynamicSources: [destinations],
})
When exactly one source is configured, the entry's dynamicSource select is
hidden and auto-set, and the parent relationship collapses to that single
collection. With multiple sources, the editor chooses a source and the parent
relationship spans the union of all configured parentCollections.
megaColumns dataEarlier versions modelled mega menus as megaColumns. Convert existing data to
the new megaEntries shape with the exported helper. It is safe to run multiple
times — already-migrated documents are skipped.
import { migrateMegaColumnsToEntries } from '@foundrykit/menu-plugin'
// `payload` is an initialised Payload instance
const { migrated, scanned } = await migrateMegaColumnsToEntries(payload)
console.log(`Migrated ${migrated} of ${scanned} menus`)
GET /api/menus/:slug
Query parameters:
locale: optional locale variantdepth: Payload relationship depth, default 0Example:
curl 'https://example.com/api/menus/main-menu?locale=en&depth=1'
GET /api/menus-export
By default this requires an authenticated admin user.
POST /api/menus-import
Content-Type: application/json
Accepts either a JSON array of menu documents or { "menus": [...] }.
Imports upsert by slug + locale.
Example response:
{
"created": 1,
"updated": 2,
"errors": []
}
fetchMenuUse fetchMenu anywhere you have a fetch implementation, including server components and non-Payload frontend code.
import { fetchMenu } from '@foundrykit/menu-plugin/rsc'
const menu = await fetchMenu({
slug: 'main-menu',
locale: 'en',
depth: 1,
baseURL: 'https://example.com',
})
useMenu'use client'
import { useMenu } from '@foundrykit/menu-plugin/client'
export function Navigation() {
const { error, isLoading, menu } = useMenu({
slug: 'main-menu',
locale: 'en',
depth: 1,
baseURL: 'https://example.com',
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error}</div>
if (!menu) return null
return <pre>{JSON.stringify(menu, null, 2)}</pre>
}
useMenu includes a 30 second client-side cache and aborts in-flight requests on unmount.
slug, locale, and depthcacheTTLMIT
payload theme
shadcn-style redesign of the Payload admin panel driven by one accent color, with dashboard, command palette and dark…
69909/wk26v3
theme management
Theme presets, live preview and WCAG contrast audit managed from the Payload admin, with SSR theme injection for Next.js
650/wk9v3
payload nav studio
Replace the Payload admin sidebar with a drag-and-drop navigation that each user can customize
640/wk13v3
payload navigation
Drag-and-drop navigation menu builder with a precomputed menu tree for frontend use
400/wk2v3
payload nested docs page tree
Drag-and-drop page tree with status badges for Payload nested docs collections in the admin list view
690/wk35v3
payload plugin info tooltip
Show field help as an info icon tooltip next to the label instead of a description under the field
670/wk6v3 v4