Map collections to Sanity document types
In Sanity, your content model is TypeScript: version-controlled, reviewable, and testable. Each Webflow collection that passes the two-question test from the previous lesson becomes a Sanity document type. AI generates the first pass from your actual API data; your job is to review and correct it.
This lesson runs two prompts in sequence. The first generates one schema file per collection. The second reviews that output, finds field groups that repeat across schemas, and extracts them into shared reusable types. Running them separately, with a review step in between, gives you a chance to catch mistakes before they compound.
Before running the prompts, set up this folder structure inside sanity/schemaTypes/:
sanity/schemaTypes/ documents/ — document types (page, blogPost, author, etc.) objects/ — reusable embedded types (seo, address, etc.) sections/ — page builder section types (heroSection, featureGrid, etc.) portableText/ — blockContent definition and embedded PT types (codeBlock, figureImage, etc.) index.ts — imports and re-exports everythingCreate the folders now:
mkdir -p sanity/schemaTypes/documents sanity/schemaTypes/objects sanity/schemaTypes/sections sanity/schemaTypes/portableTextThe separation makes it immediately clear what a file is without opening it, and keeps the AI from dumping everything into a flat list where documents and shared utility types become indistinguishable. The prompts below save files to the correct subdirectory. If the AI ignores the path instructions, move the files manually before continuing.
Webflow uses different names for "the thing you see" depending on context, and conflating them is the single most common source of wrong-field bugs. Map them before generating any schema.
The key rule: CMS collections use name in fieldData, not title. Keep it as name in your Sanity schema. Renaming it title for aesthetic reasons breaks the import transform, a direct 1:1 mapping becomes a rename, and creates confusion when page singletons use title for a different concept.
For page singletons, document title and SEO title are different fields with different sources:
// Page singleton — two separate concerns{ title: pageTitleBySlug['home'], // from pages.json display name seo: { metaTitle: extractedFromHead.title, // from HTML <title> tag metaDescription: extractedFromHead.description, }}That distinction, page name is not SEO title, saves a round trip if you establish it before schema generation.
Open your AI editor (Cursor or Claude Code) and run this prompt against the files you fetched in Chapter 2:
@webflow/api/components.json @webflow/api/collections.json @webflow/export/[page-name].html Identify the distinct section patterns across these files. For each collection that has its own page or could appear standalone, write a Sanity document type schema in TypeScript using defineType and defineField. For collections that only ever appear inside another document, write them as inline object arrays instead. Use camelCase field names, type: 'blockContent' for rich text, type: 'reference' for links to other document types, and type: 'image' for image fields. Save each document type to /sanity/schemaTypes/documents/ and update index.ts to import and re-export them all.Important registration rules:
- Every type passed to
defineTypemust be exported from its file and added to theschemaTypesarray inindex.ts. - Any custom object type used inside a Portable Text
ofarray (such ascodeBlock,htmlEmbed, orfigureImage) must be defined with its owndefineTypecall, exported, and registered inindex.ts. Defining it inline insideblockContent.tsand referencing it by name is not sufficient. Studio will show "Unknown type" for any document containing that block type.
The AI reads your actual collection field names and relationships, not a generic example, so the output maps directly to your site.
For each generated schema, check:
- Document vs. object — any collection the AI modeled as a document type should have its own page in your site. If it doesn't, it should be an inline array instead. A
postdocument type is correct; afaqItemdocument type probably isn't. - Slug field — the AI may add
source: 'title'to slug fields. Leave it out for now — imported content setsslug.currentexplicitly from your Webflow export. You can add source back post-migration for editorial UX (see the slug two-phase note below). Also check: CMS types should usesource: 'name', notsource: 'title', since Webflow CMS items usenameas the primary field. - Field names on CMS types — confirm the AI used
name(nottitle) as the primary label field for CMS collection schemas. If it generatedtitle, rename it tonamebefore continuing. This keeps the import transform a direct 1:1 mapping and avoids confusion with page singletons. - Reference fields — confirm the
toarray lists the correct target types. The AI infers these from field names, which is usually right but worth checking. - SEO fields — these are rarely in Webflow's API schema but you'll want them in Sanity. Add an
seoobject field to every document type that has a public URL if the AI didn't include one. - File locations — confirm document types landed in
documents/, not the root. Move any misplaced files before running Step 2.
Once you're satisfied with the individual schemas, move to Step 2.
The first prompt generates schemas independently, so it inlines any repeated field groups, SEO fields, image and alt combinations, address blocks, into every schema that needs them. That's valid, but it creates maintenance overhead: change the SEO structure and you're editing five files instead of one. Run this follow-up prompt to extract repeated groups into named reusable types:
@sanity/schemaTypes/documents/ Review all the schema files in this directory. Identify any field groups that appear in three or more schemas — for example, SEO fields (metaTitle, metaDescription, ogImage, noIndex), or any other repeated cluster of fields. For each repeated group: create a named object type in its own file saved to /sanity/schemaTypes/objects/ (e.g. seo.ts), replace the inlined fields in each schema with a single defineField({ name: '...', type: '...' }) reference to the new type, and update index.ts to import and register the new types. Output all modified and new files.The output should be a small number of new shared type files in objects/ and updated references in each document schema. Common shared types on most sites: seo, sometimes openGraph, sometimes an address or contact object.
After running this prompt, confirm Studio still compiles without errors before moving on — a mismatched type name between the new shared type and its reference in a schema is the most common mistake here.
A correctly generated post document type looks like this:
// sanity/schemaTypes/documents/post.tsimport { defineType, defineField } from 'sanity'
export const post = defineType({ name: 'post', title: 'Post', type: 'document', fields: [ defineField({ name: 'title', type: 'string' }), defineField({ name: 'slug', type: 'slug', // No `source` — slug is set from Webflow export, not derived from title }), defineField({ name: 'author', type: 'reference', to: [{ type: 'author' }] }), defineField({ name: 'body', type: 'blockContent' }), defineField({ name: 'seo', type: 'seo' }), ]})And a child collection correctly converted to an inline array looks like this:
defineField({ name: 'faqs', title: 'FAQs', type: 'array', of: [{ type: 'object', fields: [ defineField({ name: 'question', type: 'string' }), defineField({ name: 'answer', type: 'text' }), ] }]})Once you've reviewed and corrected the output, confirm all files are in the right subdirectory (documents/ for document types, objects/ for shared types) and that index.ts imports from those paths. Confirm Studio compiles without errors before moving on.
Gotcha: type name alignment. The name field in each schema definition is the _type string that every imported document must carry. If your schema file has name: 'blog' but your import script writes _type: 'blogPost', Studio renders each document as "Unknown type" — very difficult to diagnose after thousands of documents exist. The import.js prompt in Chapter 5 has the AI read name values directly from your schema files rather than guessing, so this shouldn't come up if you follow it. If you do see "Unknown type" after import, compare _type in a few imported documents (via the API or sanity groq '*[0]{_type}') against the name field in the corresponding schema file — they must match exactly. Don't rename types after import has run unless you're prepared to also migrate every existing document's _type field.
Gotcha: blockContent object types need separate registration. Any custom object type used inside a Portable Text array — codeBlock, htmlEmbed, figureImage — must be both defined as a named type and registered in the global schemaTypes array. Defining them inline inside blockContent.ts and referencing them by name in the of array isn't enough. The schema generation prompt above includes this requirement explicitly, so a correctly generated schema won't have this problem. If you see "Unknown type: codeBlock" (or similar) in Studio, check that the type is exported from its file and present in the schemaTypes array in index.ts.
Slug source: two-phase approach. During migration, leave source off slug fields entirely — import scripts set slug.current explicitly from fieldData.slug, and the Generate button never fires during import. After migration, add source back for editorial UX so editors get the Generate button on new documents. Use source: 'name' for CMS collection types and source: 'title' for page singletons — they use different primary fields. Migrated slugs stay untouched until an editor clicks Generate, so there's no risk of accidentally changing live URLs.
// CMS collection type (e.g. blog, faqItem)defineField({ name: 'slug', type: 'slug', options: { source: 'name', maxLength: 96 } })// Page singleton (e.g. page, homePage)defineField({ name: 'slug', type: 'slug', options: { source: 'title', maxLength: 96 } })Next you’ll design the page builder schema so editors can compose pages from section types.