Replatforming to Sanity is a chance to improve the content model, editorial workflow, and frontend data contract. Do not recreate the source CMS shape unless the project explicitly needs a temporary lift-and-shift phase.
Use a two-phase approach when risk or timeline is high:
Stabilize: migrate into a close, understandable model with stable IDs, complete assets, and working frontend queries.
Improve: remodel page-shaped content into semantic documents, references, page builder sections, or reusable entities after fidelity is proven.
For large migrations, write deterministic migration scripts, review mappings carefully, and identify edge cases before import. The migration must be repeatable and idempotent.
For implementation work, keep migration artifacts out of the application root unless the repo already has a convention:
migration/ extracted/ # raw source snapshots, usually gitignored transformed/ # generated Sanity-shaped JSON/NDJSON, usually gitignored reports/ # counts, quality issues, skipped records, validation output scripts/ extract.ts transform.ts validate.ts import.ndjson
Make generated data reproducible. Commit migration scripts and mapping docs; gitignore large exports, credentials, raw customer data, and generated imports unless the project explicitly wants fixtures.
Model content by meaning, not by legacy storage or frontend layout.
Split generic page/template records into semantic types when the page represents a real entity: person, location, event, product, caseStudy, article.
Consolidate near-duplicates such as Author, Staff, Presenter, and Expert when they represent the same real-world entity.
Use references for reusable or independently managed content: authors, people, products, categories, tags, companies, reusable testimonials, shared CTAs.
Use embedded objects for content that only belongs inside one document: SEO metadata, page-specific hero content, one-off sections, migration metadata.
Preserve source IDs and legacy URLs in a migration or migrationMetadata object unless the project already has a standard field shape.
Add schema fields before migration code depends on them, then run schema extract and TypeGen if the project uses TypeScript.
Database dumps: complete but usually more work because relationships and media often require manual joins.
Static HTML or crawling: viable fallback when no structured export exists, but plan for lower fidelity and manual review.
Prefer an official bulk export when it exists. It is usually safer than hand-crafting API calls because it is more likely to include drafts, scheduled content, assets, field definitions, and metadata. If required access or credentials are missing, stop and ask for read-only credentials or an export file rather than guessing.
Always snapshot raw extraction results before transformation. This makes the slowest part of migration, transform iteration, offline and repeatable.
Choose the write path by migration size and repeatability:
NDJSON + sanity datasets import: best for large initial loads and export-file migrations. Supports _sanityAsset directives and --replace.
sanity migration: good for reproducible scripted imports inside a Studio project, dry runs, and batched mutations.
sanity exec or custom scripts with @sanity/client: good for custom extraction/import loops, incremental syncs, or complex asset upload flows.
Sanity MCP/content tools: good for small targeted operations, inspection, and patches. Avoid them for bulk content loads when a script or NDJSON import is more reliable.
Translation metadata, redirect documents, and post-import relationship fixes.
For relationship-heavy migrations, use a Sanity-safe multi-pass import:
Upload assets and build source asset ID -> Sanity asset ID maps.
Promote reusable string lists, tags, categories, authors, products, or other shared values into reference documents.
Create primary documents with deterministic IDs and scalar fields.
Link references after every target document ID is known, either by emitting deterministic refs in NDJSON or by running a patch pass.
This pattern applies to Sanity because references are just document IDs. It is especially useful when the source stores relationships as nested objects, links, string arrays, or IDs that need lookup tables before they can become Sanity references.
Prefer the largest available original asset. Do not import multiple resized variants of the same source image.
For local files, use absolute file:///... URIs or package the NDJSON and assets into a .tar, .tar.gz, or .tgz.
During CLI import, Sanity temporarily imports references as weak and strengthens them after all documents are present; this is why _updatedAt can change for documents with references.
Use --replace for idempotent reruns, --missing when only filling gaps, and --allow-failing-assets only when missing assets should be logged for later cleanup.
Disable or pause webhooks that would be triggered by high-volume imports.
When using @sanity/client instead of CLI import:
Use low-concurrency queues and batched transactions.
Keep mutation payloads below API limits.
Prefer mutation visibility deferred for large imports when immediate queryability is unnecessary.
Use _weak: true in JSON references when creating references before targets exist. In schema definitions the property is weak; in client JSON the property is _weak.
Schema validation rules run in Studio and document validation commands, not automatically on API/client writes. Validate transformed documents before import.
Use defineType, defineField, and defineArrayMember when creating schema files.
Prefer image fields with hotspot: true and an alt field when editorial images need cropping or accessibility metadata.
Use defineQuery for GROQ queries when the project uses TypeGen.
Run schema extraction and TypeGen after schema/query changes when TypeScript types are part of the project.
Deploy or apply schema changes before using MCP/content tools against the target dataset.
Keep migration metadata small and useful: source system, source ID, source type, legacy URL, migrated timestamp, and quality flags.