Strapi → Sanity
Export Strapi as readable JSONL, then load Sanity with a migration script. Collection types become documents. Components become objects. Dynamic zones become arrays of named objects. Relations become references. Rich text becomes Portable Text. Upload assets first and keep Strapi documentId as a stable key.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Strapi
- An open-source headless CMS with a writable admin, REST, GraphQL, and a readable export archive
- Editors get a purpose-built publishing workflow instead of raw files
- Self-hosting, schema code, and custom importers become your problem when you leave a SaaS CMS
- Themes, extensions, and content models add maintenance and portability constraints
Sanity
- Content Lake, schema-as-code, and customizable Studio support sophisticated structured content
- Editors get a purpose-built publishing workflow instead of raw files
- GROQ, schema code, and custom editorial tooling demand engineering ownership
- Themes, extensions, and content models add maintenance and portability constraints
Sanity: Content Lake, schema-as-code, and customizable Studio support sophisticated structured content. This removes a major source-side concern: Self-hosting, schema code, and custom importers become your problem when you leave a SaaS CMS.
What you lose: An open-source headless CMS with a writable admin, REST, GraphQL, and a readable export archive. What you inherit: GROQ, schema code, and custom editorial tooling demand engineering ownership.
Jump to a section
Know the shape of the move.
This timeline assumes
- Strapi 5. You can run a readable export and reach the uploads files.
- You have a Sanity project and dataset you can write with a token.
- Strapi stays online until Sanity no longer needs Strapi media URLs.
- The site already documents Contentful → Sanity and Strapi → Contentful as nearby routes.
- You will write a script. There is no official one-click Strapi importer in Sanity.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Export archive | partial | critical | strapi export includes entities, links, schemas, configuration, and assets. Admin users and API tokens are omitted. Default encryption hides the JSONL until you pass --no-encrypt. | Export readable files. Keep a separate admin list. |
| Type map | manual | critical | Sanity’s Strapi notes map collection types to documents, single types to singleton documents, components to objects, and dynamic zones to arrays of named objects with one _type per __component. | Write the Sanity schema first. List every __component value. |
| Identifiers | partial | critical | The same notes say Strapi v5 documentId is the best stable source key for deterministic Sanity ids, especially with locales. | Do not use auto ids. Derive Sanity _id from documentId. |
| Relations | partial | critical | Strapi relations become Sanity references. Import referenced documents first. | Create-or-replace in dependency order. Fail if a reference is missing. |
| Assets | partial | critical | Sanity’s skill says to upload assets to Sanity or the Media Library. Do not leave production on a legacy CDN. | Upload first. Patch image fields with Sanity asset ids. |
| Rich text | partial | high | Blocks, Markdown, HTML, or CKEditor JSON must become Portable Text. Storing raw HTML is a last resort. | Convert a sample per type. Keep the source field in a hidden dump if you need to retry. |
| i18n | manual | critical | The notes say localized Strapi v5 content often maps to document-level language fields and translation metadata, with a deterministic id pattern. | Decide document-level versus field-level localization before the first import. |
| Drafts | partial | high | Decide whether unpublished Strapi rows become drafts in Sanity. | Map status. Do not publish drafts. |
| Roles and tokens | lost | high | Strapi admin users and API tokens are not in the export. Sanity members are new accounts. | Invite the team. Rotate tokens. |
| Webhooks and preview | manual | high | Strapi webhooks do not become Sanity webhooks or presentation previews. | Rebuild them. |
| Frontend queries | manual | critical | GROQ and document ids replace REST or GraphQL you used on Strapi. | Point a branch at the dataset. Run the suite. |
| History and scheduled publishes | lost | medium | Strapi history and schedules do not import as Sanity history. | Archive what you need. Recreate future publishes. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| Collection type | Sanity document type | manual | Schema first. |
| Single type | Singleton document with a fixed id | transform | One document. |
| documentId | Sanity _id | transform | Deterministic. |
| Component | Object type | transform | Keep field names where you can. |
| Dynamic zone | Array of objects keyed by __component | transform | One _type per component. |
| Relation | Reference | transform | Import targets first. |
| Media | Sanity image or file asset | transform | Upload first. |
| Rich text | Portable Text | transform | Do not leave raw HTML on published docs. |
| Locale variant | Language field or translated document | transform | Pick one model. |
| Admin user or API token | No destination | unsupported | Not in the export. |
Make the move recoverable.
Create the source-of-truth backup
Keep a readable Strapi export and a running Strapi you can point at again.
- Run a readable strapi export.
- Record type, locale, and asset counts.
- Hash the directory or archive.
- List every __component value.
- Keep a restore-tested Strapi backup.
Proof to capture: Export hash, component list, and a named restore owner.
Sanity schema
Make documents, objects, and references that match the Strapi model.
- Read schemas/*.jsonl.
- Create document and object types.
- List dynamic-zone components.
- Deploy the schema before import.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
ETL import
Load assets, then documents, then references.
- Upload assets.
- Transform entities to documents with stable ids.
- Convert rich text to Portable Text.
- Set references only after targets exist.
- Rerun until the dataset converges.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
Auto ids instead of documentId
Reruns create a second copy of every document.
- Consequence
- The dataset doubles. References point at leftovers.
- Mitigation
- Derive _id from documentId. Use createOrReplace.
Stop if: A rehearsal run creates duplicates for the same documentId.
Unknown __component
A dynamic zone value has no Sanity _type.
- Consequence
- That block disappears or the import fails.
- Mitigation
- Inventory every __component before you write the schema.
Stop if: A production page relies on an unmapped component.
Published HTML still points at Strapi
Portable Text or leftover HTML includes Strapi media URLs.
- Consequence
- Images die when you shut Strapi down.
- Mitigation
- Upload assets and rewrite URLs. Search the dataset for the old host.
Stop if: A high-traffic document still hotlinks Strapi.
Do the work in this order.
- Days 1–5 · inventory
Inventory Strapi
12–20 hours active4–5 days elapsedOwner review waiting- List types, components, locales, and media.
- Choose Sanity document ids.
Depends on: Strapi and Sanity admin access
Stop / go checkpointExport?
Go when: The type and component map is signed.
Stop when: A dynamic zone is unknown.
- Days 5–8 · backup
Readable export
6–12 hours active2–4 days elapsedExport runtime waiting- Export readable JSONL and assets.
- Hash files.
- Count rows.
Depends on: inventory
Stop / go checkpointWrite schema?
Go when: JSONL opens and counts exist.
Stop when: The archive is unreadable.
- Days 8–28 · pilot
Schema and rehearsal import
40–100 hours active12–20 days elapsedImport runtime waiting- Deploy Sanity schema.
- Upload assets.
- Import a sample, then the rest.
- Point a frontend branch at Sanity.
Depends on: backup
Stop / go checkpointCut over?
Go when: Counts, references, and Portable Text pass.
Stop when: A required component or locale is wrong.
- Days 28–35 · cutover
Freeze and switch the API
8–18 hours active3–5 days elapsedFinal export waiting- Freeze Strapi publishes.
- Re-export and rerun createOrReplace.
- Point production at Sanity.
Depends on: pilot
Stop / go checkpointSanity is the CMS?
Go when: Production reads Sanity only.
Stop when: A production page still uses Strapi.
- Days 35–40 · observe
Observe and close
6–10 hours active5–7 days elapsedPublish cycle waiting- Watch previews and asset hosts.
- Keep Strapi running.
Depends on: cutover
Stop / go checkpointClose rollback?
Go when: One publish cycle is clean.
Stop when: Ids or assets regress.
Cut over with a way back.
Cutover
Make Sanity the only CMS the production frontend reads.
- Freeze Strapi publishing.
- Run a final export and createOrReplace import.
- Search the dataset for leftover Strapi hosts.
- Switch production environment variables.
- Publish one known document and confirm the live page.
Proof to capture: Production reads Sanity. Counts match. Assets do not use Strapi URLs you will cancel.
Rollback
Return the frontend to Strapi without losing Sanity-era edits you still want.
- Point production back at Strapi.
- Export Sanity documents changed after cutover.
- Apply only reviewed documents back to Strapi if needed.
- Keep the Strapi export.
Proof to capture: Production reads Strapi again.
- Duplicate documentIds
- Unmapped __component
- Broken required asset
- Frontend still mixed between APIs
- Locale missing
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | Deterministic _id | Re-run the importer on a sample. | Counts do not double. _id matches documentId. | Id sheet. | |
V-02Blocking | Document counts | Compare Strapi and Sanity per type and locale. | Counts match or variances are signed. | Count sheet. | |
V-03Blocking | Sanity hosts | Search documents for the old media host. | Required images are Sanity assets. | Asset report. | |
V-04Blocking | Dynamic zones | Open a document that uses every __component. | Each block has a Sanity _type. | Component sample. | |
V-05Blocking | Production API | Watch network on a production build. | No Strapi API requests. | Network log. | |
V-06Blocking | Portable Text | Render a sample of converted bodies. | Headings, links, and images survive. | Render sample. | |
V-07Blocking | Language documents | Open each published locale for a sample type. | Approved locale content. | Locale sample. | |
V-08Blocking | Strapi still serves | Keep the project and a rollback env. | You can point the frontend back. | Rollback note. |
Remove the scaffolding safely.
Safe after: One publish cycle, no required Strapi media hosts, and seven stable days.
- Archive the export and importer log.
- Revoke Strapi tokens the frontend used.
- Keep Strapi running through the retention window.
- Rotate the Sanity write token used for import.
- Document dataset backup.
- Schedule the next schema and token review.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Strapi: data exportAccessed 2026-10-31
- Sanity: importing dataAccessed 2026-10-31
- Sanity agent toolkit: Strapi migration notesAccessed 2026-10-31
- Sanity agent toolkit: CMS migration skillAccessed 2026-10-31