Navigation is “small content” that changes often—and it’s one of the best candidates to move into your headless CMS. The goal is simple: editors should be able to update header/footer links without touching code, while developers keep the UI consistent and safe.
This guide shows a practical menu model and a Next.js rendering approach that works for multi-language, multi-site, and complex dropdowns.
Table of Contents
- What to model (header, footer, utility)
- Recommended menu data model
- Handling nested menus (dropdowns)
- Internal vs external links
- Multi-language menus
- Next.js rendering pattern
- Caching and invalidation
- Pitfalls to avoid
What to model (header, footer, utility)
Most sites need 3 “menu surfaces”:
- Header navigation (primary)
- Footer navigation (often multiple columns)
- Utility menu (login, docs, status, contact)
Model these as separate menus so you can change them independently and reuse them across pages.
Recommended menu data model
Keep menus explicit and predictable:
Menu
name(e.g. “Main”, “Footer”, “Docs”)slug(e.g.main,footer,docs)items[](ordered)
MenuItem
label(string)href(string)kind(enum):internal|externalnewTab(boolean)children[](optional, ordered)icon(optional)trackingId(optional)
This structure is flexible enough for dropdowns, footer columns, and utility menus—without turning navigation into a CMS-freeform rich text blob.
Handling nested menus (dropdowns)
Two levels is usually enough:
- Parent item (e.g. “Solutions”)
- Children items (e.g. “For Agencies”, “For SaaS”, “For E-commerce”)
If you need deeper nesting, consider:
- limiting depth to 3 max
- adding a “mega menu” section type rather than infinite recursion
For footer columns, you can treat “column” as a parent item with children.
Internal vs external links
Avoid “magic string” URLs that editors can break. Two common approaches:
Option A (simplest): store href as a string
- Editors paste
/blog,/docs,https://… - Developers validate at runtime (recommended)
Option B (safer): internal link references
Store one of:
internalPath(string like/blog/[slug])- or a reference to a CMS entry (Page, Post, Category)
Then your frontend resolves it to a stable route. This is great for big sites but takes more implementation effort.
Multi-language menus
If you have locales, pick one of these patterns:
Pattern 1: separate menu per locale
main-en,main-de,main-tr
Pros: easy editorial control.
Cons: duplication across locales.
Pattern 2: one menu with localized labels
labelbecomeslabel_en,label_de, …- or a
labelobject keyed by locale
Pros: single menu structure.
Cons: CMS UI can be less friendly if localization support is limited.
Whichever you choose, be consistent with your site routing (e.g. /en/... vs domain per locale).
Next.js rendering pattern
The cleanest pattern in Next.js App Router:
- fetch menu data in a server component (Header/Footer)
- render using
next/linkfor internal links - ensure tokens stay server-side (don’t fetch from the browser)
Pseudo-code:
import Link from 'next/link';
function NavItem({ item }) {
const isExternal = item.kind === 'external' || item.href.startsWith('http');
if (isExternal) {
return (
<a href={item.href} target={item.newTab ? '_blank' : '_self'} rel="noreferrer">
{item.label}
</a>
);
}
return <Link href={item.href}>{item.label}</Link>;
}
For dropdowns:
- render parent as button/link
- render children in a popover/menu
- always handle the case where
childrenis empty
Caching and invalidation
Menus are perfect for caching:
- Cache aggressively (they change rarely compared to page views)
- Invalidate on publish using webhooks (best) or short TTL
Recommended:
- Cache menu fetches on the server (framework cache / fetch cache)
- Invalidate when content changes (e.g. “menu updated” webhook)
If you’re already using ISR or tag-based revalidation in Next.js, you can tie menu updates to a single “navigation” tag.
Pitfalls to avoid
- Unvalidated URLs: add a simple runtime validator to prevent
javascript:or malformed URLs. - Editor can break the site: keep a fallback menu in code or render a safe default when menu fetch fails.
- Over-modeling: don’t try to represent every layout pixel in navigation; keep it data-first.
- No ownership: decide who owns navigation changes and add a review step if needed.
Related posts:
- How to Model Relations in a Headless CMS (Beginner Guide) – Relations and nested structures for linking menu items, parent/child menus, and references.
- Building a Multilingual Blog with Next.js and ElmapiCMS Headless CMS – Multi-language patterns that apply to localized menus and routing.
- Headless CMS Content Modeling: How to Build Reusable Page Sections (Blocks) – Design flexible, reusable content structures (menus are another example).
- Next.js + ElmapiCMS Starter Template: Complete Setup Guide – Fetch and render CMS content (including menus) in Next.js with caching.
