# Modeling navigation in Contentstack

- **Authors:** Lo Etheridge
- **Published:** 2026-03-17T12:14:29.698Z
- **Updated:** 2026-03-27T11:04:11.834Z
- **Tags:** content_modeling, api, visual_editing

---

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)

```json
{
  "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)

```json
{
  "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)

```json
{
  "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).




