# Model navigation in Sanity

https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/model-navigation

Create a recursive navItem schema and a navigationMenu document type that editors can update without touching code.

---

## Navigation

**Course:** [Migrating from Webflow to Sanity](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity) · [View as markdown](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity.md)

**Previous:** [Model custom-template pages as singletons](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/custom-template-singletons) · **Next:** [Create a siteSettings singleton](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/site-settings-singleton)

---

## Course Contents

1. [Introduction](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/introduction-to-webflow-migration)
2. [Audit your Webflow content](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/audit-webflow-content)
3. [Recognize Webflow's content modeling patterns](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/recognize-content-modeling-patterns)
4. [Export from Webflow and connect the Data API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/export-webflow-connect-api)
5. [Initialize Sanity and install the migration toolkit](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/initialize-sanity-migration-toolkit)
6. [Map collections to Sanity document types](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/map-collections-to-schemas)
7. [Build a page builder with a sections array](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/page-builder-sections-array)
8. [Model custom-template pages as singletons](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/custom-template-singletons)
9. **Model navigation in Sanity** *(current)*
10. [Create a siteSettings singleton](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/site-settings-singleton)
11. [Handle internal links in Portable Text](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/internal-links-portable-text)
12. [Fetch your collection items from the API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/fetch-collection-items-api)
13. [Run the asset pre-upload pass](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/asset-pre-upload)
14. [Resolve reference fields from API data](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/resolve-reference-fields)
15. [Convert rich text to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/convert-rich-text-portable-text)
16. [Extract static page content using the page DOM API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/extract-static-page-content)
17. [Import content with the two-pass pattern](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/two-pass-import)
18. [Validate your import in Studio](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/validate-import-studio)
19. [Set up forms, redirects, and sitemap](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/forms-redirects-sitemap)
20. [Set up draft mode and visual preview](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/draft-mode-visual-preview)
21. [Validate your import without a frontend](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/validate-migration)
22. [Launch checklist and DNS transfer](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/launch-checklist-dns)
23. [AI prompt library and resources](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/appendix-prompt-library-resources)

---

For sites with a single navigation structure, storing it in siteSettings is a reasonable choice: one document, straightforward to query. When your site has multiple named navigations (primary, footer, mobile, utility) or serves multiple channels, a dedicated navigationMenu document type is the better fit: each menu is independently editable, individually queryable, and easy to extend without touching global settings.



The approach below uses a separate document type. If your site has only one nav, you can simplify by replacing this with a `navigation` field directly on `siteSettings`.



### Two sources of navigation data



Navigation data comes from two places, and conflating them causes empty dropdowns. Menu structure, labels, static links, and nesting depth come from the HTML export:  the "Tags" dropdown exists, the Blog link has an href. Dropdown children populated from a CMS collection come from the collection item NDJSON files plus id-map.json: the individual tag, attribute, or blog links themselves.



Webflow's static HTML export renders CMS-driven dropdown lists as empty `w-dyn-list` placeholders. The actual items only render when the site runs with live CMS binding. If your nav has dropdowns populated from a CMS collection, the HTML gives you the structure but not the content.



### Step 1: Generate navigation schemas from HTML



```text
@webflow/export/index.html @sanity/schemaTypes/index.ts Extract all navigation menus from the HTML file — identify each distinct nav (primary, footer, mobile, etc.) along with its items, labels, links, and any dropdown nesting. Then write two Sanity schemas in TypeScript: a navItem object type with a label field, a link reference field pointing to all document types with slug fields (from index.ts), an externalUrl field, and a recursive children array of navItem; and a navigationMenu document type with a title, an identifier string field, and an items array of navItem. Save navItem to /sanity/schemaTypes/objects/navItem.ts and navigationMenu to /sanity/schemaTypes/documents/navigationMenu.ts. Update index.ts to import and register both.
```

Every `navItem` object at every nesting level must include a `_key` field so Studio doesn't show "Missing keys" validation errors. Use the item's Webflow ID, slug, or a stable string as the key.



### Step 2: Seed CMS dropdown children from NDJSON (if applicable)



If your nav has dropdowns populated from a CMS collection (Tags, Categories, Products, etc.), generate a seeding script after import is complete and `id-map.json` exists:



```text
@webflow/export/index.html @webflow/api/items/ @webflow/api/id-map.json @sanity/schemaTypes/documents/navigationMenu.ts

The main nav has CMS-driven dropdowns. The HTML export shows the dropdown structure but w-dyn-list placeholders instead of actual items. Read the relevant collection NDJSON files to get the actual items. Build a seed-navigation.js script that:

1. Reads the HTML to determine which dropdowns correspond to which collections
2. Loads the relevant NDJSON items and maps their Webflow IDs through id-map.json to Sanity _id values
3. Constructs recursive navItem arrays with { _key, label, link: { _type: 'reference', _ref } } leaves and { _key, label, children: [...] } parents
4. Upserts navigationMenu documents with fixed _id values (e.g. navigationMenu.main-nav, navigationMenu.footer)
5. Saves to scripts/seed-navigation.js
```

### What the output should look like



The AI reads your actual nav structure and generates reference targets from your real document types. The schemas follow this shape:



```typescript
// sanity/schemaTypes/objects/navItem.ts
export const navItem = defineType({
  name: 'navItem',
  title: 'Navigation Item',
  type: 'object',
  fields: [
    defineField({ name: 'label', type: 'string' }),
    defineField({
      name: 'link',
      type: 'reference',
      to: [
        // Your actual document types with slug fields — not page/post
        { type: 'page' },
        { type: 'caseStudy' },
      ]
    }),
    defineField({ name: 'externalUrl', type: 'url' }),
    defineField({
      name: 'children',
      title: 'Dropdown items',
      type: 'array',
      of: [{ type: 'navItem' }]
    })
  ]
})
```

```typescript
// sanity/schemaTypes/documents/navigationMenu.ts
export const navigationMenu = defineType({
  name: 'navigationMenu',
  title: 'Navigation Menu',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({
      name: 'identifier',
      type: 'string',
      description: 'Used to query this menu in code — e.g. "main-nav", "footer-nav"'
    }),
    defineField({
      name: 'items',
      type: 'array',
      of: [{ type: 'navItem' }]
    })
  ]
})
```

The `identifier` field is how your frontend queries a specific menu without hardcoding a document ID:



```groq
*[_type == "navigationMenu" && identifier == "main-nav"][0] {
  items[] {
    label,
    link->{ slug },
    externalUrl,
    children[] { label, link->{ slug }, externalUrl }
  }
}
```

Create one navigationMenu document per named menu in your site. Pin them in Structure Builder so editors can find them without searching through document lists.



Next, you’ll set up a siteSettings singleton for global configuration.



---

## Related Resources

- [Full course as markdown](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://www.sanity.io/learn/resource/feedback

```json
{
  "path": "/learn/course/migrating-content-from-webflow-to-sanity/model-navigation.md",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>
