CoursesMigrating from Webflow to SanityModel navigation in Sanity
Migrating from Webflow to Sanity

Model navigation in Sanity

Create a recursive navItem schema and a navigationMenu document type that editors can update without touching code.
Log in to mark your progress for each Lesson and Task

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.

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.

@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.

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:

@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

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

// 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' }]
})
]
})
// 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:

*[_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.

Mark lesson as complete
You have 1 uncompleted task in this lesson
0 of 1