Modeling navigation in Contentstack
This guide shows how to model navigation in Contentstack using content types, references, Global Fields, and Modular Blocks. You will build three navigation models that build on each other:
Simple navigation: internal and external links
Drop-down navigation: internal and external links under a parent item
Mega menu: internal and external links, nested drop-down items, plus optional CTAs and images per item (not per column)
When to use it
Use these patterns when:
Marketing and product teams need to manage navigation without deploys
Navigation must link to multiple internal page types (for example Page, Landing Page)
You need predictable front-end rendering while keeping authoring simple
Prerequisites
A Contentstack stack with access to Content Types, Global Fields, and Modular Blocks
At least two internal page content types (examples used below): Page and Landing Page
A front end that can resolve referenced entries to URLs (via a url/slug field, or your routing layer)
How it works
The core problem is modeling a “link” that can point to either:
An internal entry (one of your page-like content types)
An external URL
In Contentstack, you typically solve this by creating a reusable Link Global Field using a Modular Block with two mutually exclusive options: Internal and External. Navigation items then compose these links and add structure (drop-downs, mega menu content) around them.
Assumptions used in this guide
Internal destinations are references to specific page content types (for example Page and Landing Page).
Each page type exposes a URL field (for example url or slug) that the front end can use.
Navigation is modeled as a dedicated content type so it can be versioned, localized, and referenced by pages/layouts.
Step by step
Step 1 — Create a reusable Link Global Field
Create a Global Field named link. This will be reused across all navigation use cases.
Global Field: link
label (Single line text, required)
link (Modular Blocks, required) with blocks:
internal
entry (Reference, required; limit to: Page, Landing Page)
open_in_new_tab (Boolean, default false)
external
url (Single line text, required)
open_in_new_tab (Boolean, default true)
Authoring rule: allow exactly one block instance in link. This makes it explicit that a link is either internal or external.
Common pitfalls
Allowing multiple blocks in the Link field. If authors can add both internal and external, your rendering logic becomes ambiguous.
Not restricting reference content types. Keep internal links constrained to your page-like types to avoid “linking to anything”.
Use case 1 — Simple navigation (internal + external links)
What this does
Models a flat list of navigation items, where each item is a link (internal or external).
When to use it
Top bars and footers with one level of links
Early project phase where you want a stable, minimal model
Content Type: navigation_simple
title (Single line text, required)
items (Group, multiple, required)
Group: items[]
item (Global Field: link, required)
How it works
items[] is an ordered list. The front end renders each item as a single anchor based on whether the link is internal or external.
Example entry shape (simplified)
{
"title": "Header nav",
"items": [
{
"item": {
"label": "Home",
"link": [{ "internal": { "entry": { "uid": "home" }, "open_in_new_tab": false } }]
}
},
{
"item": {
"label": "Docs",
"link": [{ "external": { "url": "https://docs.example.com", "open_in_new_tab": true } }]
}
}
]
}Common pitfalls
Missing ordering requirements. Confirm the field preserves order (Groups do) and render in that order.
Assuming referenced entries include URL by default. Your delivery query needs to include the referenced fields required to build URLs.
Next steps
Once flat navigation is stable, add drop-down behavior by allowing a parent item to optionally contain child links.
Use case 2 — Drop-down navigation (internal + external links)
What this does
Models top-level navigation items that can either be a direct link or a parent item containing a drop-down list of links.
When to use it
Header navigation with one drop-down level
Information architecture where top-level categories expand into child links
Approach
Use a single nav item structure where each top-level item can be one of:
A direct link
A drop-down with child links
Global Field: nav_item_dropdown
Create a Global Field named nav_item_dropdown using a Modular Block. This keeps the entry shape explicit and avoids optional-field ambiguity.
item (Modular Blocks, required; allow exactly one block) with blocks:
link_item
link (Global Field: link, required)
dropdown_item
label (Single line text, required)
children (Group, multiple, required)
Group: dropdown_item.children[]
link (Global Field: link, required)
Content Type: navigation_dropdown
title (Single line text, required)
items (Group, multiple, required)
Group: items[]
item (Global Field: nav_item_dropdown, required)
How it works
link_item renders as a standard navigation link.
dropdown_item renders as a label (or button) that expands into children[].
Example entry shape (simplified)
{
"title": "Header nav",
"items": [
{
"item": {
"item": [
{ "link_item": { "link": { "label": "Pricing", "link": [{ "internal": { "entry": { "uid": "pricing" } } }] } } }
]
}
},
{
"item": {
"item": [
{
"dropdown_item": {
"label": "Products",
"children": [
{ "link": { "label": "Product A", "link": [{ "internal": { "entry": { "uid": "product-a" } } }] } },
{ "link": { "label": "API", "link": [{ "external": { "url": "https://api.example.com" } }] } }
]
}
}
]
}
}
]
}Common pitfalls
Making both a link and children optional in the same object. That produces “sometimes link, sometimes group” logic that breaks validation and authoring.
Not defining click behavior for drop-down labels. Decide whether the parent label is clickable or only toggles the menu. If clickable, model it as a link too (see next use case).
Next steps
If you need richer navigation content (promos, CTAs, images) and deeper nesting, introduce a mega menu model that supports multiple item types and optional media per item.
Use case 3 — Mega menu / complex navigation
What this does
Models a navigation system that supports:
Internal and external links
Drop-downs with nested items
Optional CTA and images per item (not per column)
When to use it
Large sites where navigation contains promoted items, editorial content, or visual tiles
Multiple navigation “item types” in the same menu (link, group, promo)
When you need explicit structure to keep rendering deterministic
Approach
Use a single Navigation content type with a list of top-level items. Each item is a nav node with a specific type. Nodes can contain children, enabling nested structures. Promo content (CTA, image) is attached to nodes, not to columns.
Global Field: nav_promo
Create a Global Field named nav_promo:
eyebrow (Single line text, optional)
description (Single line text or JSON RTE, optional)
image (Asset, optional; restrict to images if your governance requires it)
cta (Global Field: link, optional)
Global Field: nav_node
Create a Global Field named nav_node using Modular Blocks (required; allow exactly one block). This enforces a single node type per item.
node (Modular Blocks) with blocks:
link_node
link (Global Field: link, required)
promo (Global Field: nav_promo, optional)
dropdown_node
label (Single line text, required)
link (Global Field: link, optional; use if the parent label is clickable)
items (Group, multiple, required)
promo (Global Field: nav_promo, optional)
Group: dropdown_node.items[]
item (Global Field: nav_node, required)
This recursion (a node containing a list of nodes) supports deeper nesting while keeping each level typed and predictable.
Content Type: navigation
title (Single line text, required)
location (Dropdown, required; example values: header, footer)
items (Group, multiple, required)
Group: items[]
item (Global Field: nav_node, required)
How it works
Top-level items[] are rendered in order.
Each item is either a link_node or dropdown_node.
dropdown_node contains nested items[] of the same nav_node type, allowing multi-level menus.
Any node can optionally carry promo (image + CTA + copy). This is per item, not per column.
Example entry shape (simplified)
{
"title": "Header nav",
"location": "header",
"items": [
{
"item": {
"node": [
{
"dropdown_node": {
"label": "Solutions",
"link": { "label": "All solutions", "link": [{ "internal": { "entry": { "uid": "solutions" } } }] },
"promo": {
"eyebrow": "New",
"description": "See what’s new this quarter",
"image": { "uid": "asset_uid" },
"cta": { "label": "Explore", "link": [{ "internal": { "entry": { "uid": "whats-new" } } }] }
},
"items": [
{
"item": {
"node": [
{
"link_node": {
"link": { "label": "E-commerce", "link": [{ "internal": { "entry": { "uid": "ecommerce" } } }] }
}
}
]
}
},
{
"item": {
"node": [
{
"dropdown_node": {
"label": "Developers",
"items": [
{
"item": {
"node": [
{
"link_node": {
"link": { "label": "API Docs", "link": [{ "external": { "url": "https://docs.example.com" } }] }
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}
}
]
}Common pitfalls
Unbounded depth. Recursive models are powerful, but teams often need a maximum depth (for example 2 or 3 levels) to keep UX and accessibility manageable. If you need a hard limit, enforce it in editorial guidelines and front-end validation.
Promo content without rendering rules. Define where promos appear (for example right rail, inline tile) and what happens when both an image and CTA are missing.
Overusing references for navigation nodes. Keep nodes embedded in the navigation entry unless you truly need node reuse across multiple navigations. Reference-heavy navigation increases query complexity and makes preview/debugging harder.
Inconsistent internal URL resolution. Ensure all referenced page types expose the fields your router needs, and your query includes them.
Next steps
Define a routing contract for internal pages (for example each page type must have url).
Add validation in your front end (or webhook) to enforce maximum depth and required fields per node type.
Decide how navigation is selected per locale/site (for example one navigation entry per locale and location).