# How do you migrate a Drupal site to Sanity, step by step?

Reviewed by Martin Dejnicki · facts verified 25 October 2026 · canonical: https://deploi.ca/sanity/migration/drupal-to-sanity

## Short answer

A Drupal to Sanity migration is a script your developers write and rerun. It exports each content type through JSON:API, an application programming interface (API) module in Drupal core ([Drupal.org](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module), October 2026). It maps fields to Sanity schema types, converts HTML to Portable Text and imports with Drupal's IDs, so reruns hit the same documents ([Sanity Docs](https://www.sanity.io/docs/content-lake/importing-data), October 2026).

- **Export route, Drupal 8.7 and later:** Core JSON:API, no configuration needed. Resources follow Drupal entity types and bundles, so each content type exports as its own collection; Drupal 7 needs added modules ([Drupal.org, JSON:API module](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module); [Sanity, The Drupal developer's introduction to Sanity.io](https://www.sanity.io/blog/sanity-vs-drupal7); [Drupal.org, Drupal 8.7.0 release notes](https://www.drupal.org/project/drupal/releases/8.7.0))
- **Rich text converter:** htmlToBlocks() in @portabletext/block-tools. Headings, lists and paragraphs convert by default; images need custom rules, and built-in tables need Studio v6.6.0 and a declared table type ([Sanity Learn, Converting HTML to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-html-to-portable-text); [Sanity Docs, Configure the Portable Text Editor](https://www.sanity.io/docs/studio/portable-text-editor-configuration))
- **References on import:** Weak first, strong once every document is in. Import order doesn't matter, and IDs may use letters, numbers, hyphens and underscores, so node-123 keeps the Drupal node ID ([Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data))
- **Roles on Growth:** 5 fixed roles, none scoped to a content type. Custom roles, the match for Drupal's per-content-type permissions, are Enterprise only, as of October 2026 ([Sanity Docs, Roles and permissions](https://www.sanity.io/docs/content-lake/roles-concepts); [Sanity, Plans that scale with you](https://www.sanity.io/pricing))

## Deploi's call

Move a single Drupal site in one cutover with a re-runnable import, and move a multisite or a heavily moderated site one site or section at a time. Either way, remodel as you go: Paragraphs become arrays of object types, HTML becomes Portable Text, and revisions stay behind in an archived Drupal database.

**MIGRATE: Move a Drupal site to Sanity in one cutover when**

- One site, one front end and one editorial team, so one re-runnable import and one redirect map cover everything.
- The new front end launches with every content type at once, because a half-moved site needs two systems serving one domain.
- Content Moderation is light enough that drafts plus Scheduled drafts cover it, so Sanity's Growth plan fits as of October 2026.

**PHASE: Move a Drupal site to Sanity in phases when**

- A Drupal multisite runs several sites, so move one site at a time and keep Drupal serving the rest.
- One section, such as a blog or a resource library, can launch first, because its URLs can be routed separately.
- Editors depend on per-content-type permissions or multi-stage moderation, so prove the Sanity plan on one section first: Sanity's docs put custom roles on Enterprise only and Sanity Workflows in early access, as of October 2026.

## How do you get content out of Drupal for a move to Sanity?

You get content out of Drupal 8.7 and later through core's JSON:API module, and out of Drupal 7 through added modules.

- **Drupal 8.7 and later.** JSON:API, a core application programming interface (API) since 8.7.0 ([Drupal 8.7.0 release notes](https://www.drupal.org/project/drupal/releases/8.7.0), October 2026), is "a zero-configuration required, opinionated" way to read a site's content, Drupal.org says. Its resources follow Drupal's entity types and bundles, so each content type exports as its own collection ([Drupal.org, JSON:API module](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module), October 2026).
- **Drupal 7.** Core has no JSON:API, so you install extra modules to get one ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026).

Which export tool fits your Drupal version compares the routes. Sanity Learn has no Drupal course ([Sanity Learn](https://www.sanity.io/learn/track/replatforming-to-sanity), October 2026), though its WordPress course says many of its techniques transfer to HTML-based content management systems like Drupal ([Sanity Learn](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/introduction-to-wp-migration), October 2026). For AI agents, Sanity's MCP (Model Context Protocol) server lists a guided Drupal path ([Sanity blog](https://www.sanity.io/blog/sanity-remote-mcp-server-is-generally-available), October 2026), and Sanity's migration skill has a Drupal guide covering "source-specific extraction routes" ([Sanity agent toolkit](https://github.com/sanity-io/agent-toolkit/blob/main/skills/sanity-migration/SKILL.md), October 2026). Sanity calls that skill "agent-scaffolded, not fully automated" ([Sanity blog, August releases](https://www.sanity.io/blog/august-releases-2026), October 2026). This playbook assumes you've decided to leave Drupal; [whether to upgrade to Drupal 11 or replatform](https://deploi.ca/sanity/migration/leaving-drupal) is its own question. Our view of Drupal rests on its documentation and public sources.

## How do Drupal content types and fields map to Sanity schema types?

Each Drupal content type becomes a Sanity document type, and each Drupal field becomes a field you define in code on that type, with a validation rule where Drupal had one. Sanity starts blank: "There are no content types setup with a clean installation" ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026). A list field's allowed values move to a string field's `options.list` of "predefined values" ([Sanity Docs, String](https://www.sanity.io/docs/studio/string-type), October 2026).

**Summaries, links, authors and menus need modeling; the rest map directly**

| Drupal field or item | Sanity type | Migration note |
| --- | --- | --- |
| Node of a content type | Document of its own type | ID built from the node ID |
| Text with summary | Portable Text plus a text field | Summary becomes a field |
| Plain text | string or text | text for long values |
| Formatted text | Portable Text array | Converted from HTML |
| Link | url, or an object with url and title | Internal links become references |
| Date | date or datetime | Check time zones |
| List with allowed values | string with options.list | Copy the allowed values |
| Boolean | boolean | None |
| URL alias | slug | The alias each node has today |
| Author | Reference to an author document | Bylines aren't Sanity members |
| Menu link | Navigation document with references | Each menu becomes data |

Sources: [Sanity, The Drupal developer's introduction to Sanity.io](https://www.sanity.io/blog/sanity-vs-drupal7); [Sanity Docs, String](https://www.sanity.io/docs/studio/string-type); [Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data); [Sanity Learn, Converting HTML to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-html-to-portable-text). As of 24 October 2026.

## How do Drupal entity references and taxonomy map to Sanity references?

Drupal entity references become Sanity references, and the import makes their order irrelevant: "During import, all references are automatically set to weak, then flipped to strong after all documents are in place" ([Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data), October 2026). Sanity accepts any ID made of letters, numbers, hyphens and underscores, so an ID such as `node-123` or `term-45` keeps the Drupal entity ID and lets every rerun hit the same document. Drupal taxonomy terms can have hierarchies ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026), so each term becomes a document with a parent reference.

**References keep Drupal IDs, and each vocabulary becomes a document type**

| Drupal structure | Sanity equivalent | Migration note |
| --- | --- | --- |
| Entity reference to a node | reference, or an array of references | Multi-value fields become arrays |
| Reference to a taxonomy term | reference to a term document | One document type per vocabulary |
| Term hierarchy | parent reference on the term | Rebuild the tree from parent IDs |

Sources: [Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data); [Sanity, The Drupal developer's introduction to Sanity.io](https://www.sanity.io/blog/sanity-vs-drupal7). As of 24 October 2026.

Two Drupal structures leave this mapping. Revisions stay behind in an archived Drupal database (Deploi's judgment), and Drupal translations get their own playbook because Drupal.org lists the translation support in core's JSON:API module, an application programming interface (API), as "Basic functionality" ([Drupal.org, JSON:API module](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module), October 2026).

## What do Drupal HTML body fields, Paragraphs and Layout Builder become in Sanity?

Drupal HTML body fields become Portable Text, Paragraphs become arrays of object types, and Layout Builder overrides need a modeling decision before any script runs. Portable Text is Sanity's JSON-based format for rich text storage ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026); how Portable Text stores rich text explains it.

- **HTML body fields.** `htmlToBlocks()` from `@portabletext/block-tools` converts headings, lists and paragraphs by default, and images need custom rules ([Sanity Learn, Converting HTML to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-html-to-portable-text), October 2026). HTML tables have a target: the Portable Text Editor has built-in table editing from Studio v6.6.0, off by default, once you declare a `table` schema type ([Sanity Docs, Configure the Portable Text Editor](https://www.sanity.io/docs/studio/portable-text-editor-configuration), October 2026).
- **Paragraphs.** The module lets editors pick predefined Paragraph types instead of one WYSIWYG (what you see is what you get) body field ([Drupal.org, Paragraphs](https://www.drupal.org/project/paragraphs), October 2026). Each Paragraph type becomes an object type in an array, the way Sanity models a page builder ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026), and a nested Paragraphs field becomes an array inside that object (Deploi's judgment).
- **Layout Builder.** It can "override the default display on individual items such as nodes" ([Drupal.org, Layout Builder](https://www.drupal.org/docs/8/core/modules/layout-builder), October 2026), so each overridden node carries its own layout. Decide per override: a page builder array, or the default template.

Pages built in Acquia Site Studio follow their own path to Sanity.

## What happens to Drupal media, files and images in a move to Sanity?

Drupal media, files and images become Sanity image and file assets, uploaded by the import itself through the `_sanityAsset` property. In each NDJSON (newline-delimited JSON) line, `_sanityAsset` sits where an asset reference would go, prefixed `image@` or `file@`, with an absolute file path or a public URL ([Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data), October 2026). The import fails when an asset is unavailable unless you pass `--allow-failing-assets`, so check every Drupal file URL before the run and log the ones that 404.

- **Alt text and captions.** Copy them into fields on the Sanity image object, so they travel with every use (Deploi's judgment).
- **Shared files.** A Drupal media entity used on many nodes is one file. Sanity gives an uploaded image an ID derived from the image itself, so uploading the same image twice never creates a duplicate. Uploading through the JavaScript client also lets you record the Drupal media ID in the asset's `source` key, so a rerun can skip files already uploaded ([Sanity Learn, Uploading assets efficiently](https://www.sanity.io/learn/course/refactoring-content/uploading-assets-efficiently), October 2026).
- **Media Library.** Sanity's organization-wide asset app is an add-on for certain Enterprise plans as of October 2026 ([Sanity Docs, Media Library introduction](https://www.sanity.io/docs/media-library/introduction); [Sanity pricing](https://www.sanity.io/pricing)). A Drupal move can use ordinary dataset assets instead, which need no add-on.

## How do you keep Drupal URL aliases and redirects working after a move to Sanity?

Keep Drupal URLs by exporting each node's current URL alias into a Sanity slug, and each Redirect module entry into a redirect document your front end reads. Pathauto builds aliases from a pattern system "that uses tokens which the administrator can change" ([Drupal.org, Pathauto](https://www.drupal.org/project/pathauto), October 2026), so export the alias each node has today, not the pattern: nodes published under an older pattern can keep older aliases (Deploi's judgment).

The Redirect module lets editors "create manual redirects and maintain a canonical URL for all content" ([Drupal.org, Redirect](https://www.drupal.org/project/redirect), October 2026). Each one becomes a `redirect` document with a source, a destination and a permanent flag, the model in Sanity's redirect guide ([Sanity Docs, Managing redirects with Sanity](https://www.sanity.io/docs/developer-guides/managing-redirects-with-sanity), October 2026). That guide notes that in Drupal, redirects are "typically a plugin away", while a headless front end needs integration work to read them. Add Drupal's system paths, such as `/node/123`, as redirects too, because old links and feeds still use them (Deploi's judgment). [Keeping search rankings through the move](https://deploi.ca/sanity/migration/cms-to-sanity/seo-preservation) carries the full SEO (search engine optimization) checklist.

## What replaces Drupal roles, Content Moderation, Views and modules like Webform and Search API?

Drupal's roles, moderation and modules, including Search API (a search application programming interface module), don't migrate. Each becomes a Sanity setting, a GROQ (Graph-Relational Object Queries) query in the front end, or a service. Drupal grants editor permissions "based on role and content type" ([Sanity's Drupal guide](https://www.sanity.io/blog/sanity-vs-drupal7), October 2026). Sanity's Growth plan has five fixed roles that span whole datasets (Contributor writes drafts "within all datasets"), so none limits an editor to one content type. Custom roles are Enterprise only as of October 2026 ([Sanity Docs, Roles and permissions](https://www.sanity.io/docs/content-lake/roles-concepts); [Sanity pricing](https://www.sanity.io/pricing)). Drupal's Content Moderation adds states beyond published and unpublished ([Drupal.org](https://www.drupal.org/docs/8/core/modules/content-moderation/overview), October 2026); Sanity Workflows is in early access ([Sanity Docs](https://www.sanity.io/docs/workflows/cookbook-editorial-review); [early access terms](https://www.sanity.io/docs/workflows/prerelease), October 2026).

**Each Drupal feature has a Sanity replacement or a service**

| Drupal feature | Sanity replacement | Note, October 2026 |
| --- | --- | --- |
| Roles by content type | Custom roles | Enterprise only |
| Light moderation | Drafts plus Scheduled drafts | Growth; Studio v4.14.0 or later |
| Coordinated launches | Content Releases | Enterprise add-on |
| Multi-stage review | Workflows | Early access |
| Views | GROQ queries | Rewrite each listing |
| Webform | A form service or @sanity/form-toolkit | Toolkit covers Mailchimp, HubSpot |
| Search API | GROQ text search or an Algolia sync | Sync runs as a Sanity Function |

Sources: [Sanity Docs, Roles and permissions](https://www.sanity.io/docs/content-lake/roles-concepts); [Sanity, Plans that scale with you](https://www.sanity.io/pricing); [Sanity Docs, Scheduled drafts](https://www.sanity.io/docs/studio/scheduled-drafts); [Sanity Docs, Content Releases user guide](https://www.sanity.io/docs/user-guides/content-releases); [Sanity Docs, Cookbook: Editorial review](https://www.sanity.io/docs/workflows/cookbook-editorial-review); [Drupal.org, Concept: Uses of Views](https://www.drupal.org/docs/user_guide/en/views-concept.html); [Sanity Docs, Forms with Sanity](https://www.sanity.io/docs/developer-guides/forms-with-sanity); [Sanity Docs, Search text content with GROQ](https://www.sanity.io/docs/content-lake/search-content-with-groq); [Sanity Docs, How to implement front-end search with Sanity](https://www.sanity.io/docs/developer-guides/how-to-implement-front-end-search-with-sanity). As of 24 October 2026.

Site search on Sanity compares the search options.

## What is the step-by-step plan for a Drupal to Sanity migration?

A Drupal to Sanity migration runs in ten steps, and the import runs many times. It fails on any document ID that already exists, so reruns pass `--replace` with the same Drupal-derived IDs ([Sanity Docs](https://www.sanity.io/docs/content-lake/importing-data), October 2026).

**Migrating a Drupal site to Sanity**

1. **Inventory.** Count nodes per type, Paragraph types, vocabularies, Views and modules.
1. **Cut list.** Drop revisions, unused types, generated listings and modules with no front-end effect, because nothing reads them.
1. **Schema.** Write one document type per content type and one object type per Paragraph type.
1. **Export.** Pull JSON:API (application programming interface) collections, or Drupal 7 tables.
1. **Transform.** Map fields, run `htmlToBlocks()` and derive each ID from its Drupal ID.
1. **Stage.** Import into a staging dataset with webhooks off.
1. **Validate.** Run `sanity documents validate` ([Sanity Docs](https://www.sanity.io/docs/content-lake/important-considerations-for-schema-and-content-migrations)) and match counts.
1. **Front end.** Rewrite Views as queries and load the redirects.
1. **Cutover.** Freeze Drupal edits, rerun with `--replace`, then switch traffic.
1. **Read-only.** Keep Drupal read-only until every old URL resolves.

> **Where Drupal migrations break**
>
> - Nested Paragraphs the export misses (Deploi's judgment).
> - Layout Builder overrides stored per node.
> - HTML tables with no conversion rule.
> - Multisite: every site has its own database ([Drupal.org](https://www.drupal.org/docs/getting-started/multisite-drupal)), so node IDs repeat; prefix them by site. Cross-dataset references need Enterprise as of October 2026 ([Sanity Docs](https://www.sanity.io/docs/studio/cross-dataset-references)).

## Deploi's point of view

**Where we disagree.** The common advice treats a Drupal migration as an export problem. Getting content out is the easy part, because core JSON:API exposes it without configuration. The work is in the decisions inside the content: nested Paragraphs, Layout Builder overrides stored per node, HTML tables, and aliases built from patterns that changed over the years. A script that copies those one to one moves Drupal's structure into Sanity and keeps the problems you left Drupal to fix. The second mistake is migrating revision history: editors rarely read it, and every revision you import is another document to count, store and validate.

**What we've seen.** Two problems a Drupal move raises are ones we've handled on deploi.ca's own Sanity content. Rich text: our Shopify FAQ hub stores tables as a custom Portable Text block type with a caption, sources and an as-of date, and 229 of its 256 live answers carried at least one table on 24 October 2026. Re-runnable imports: we ship content as packages with fixed document IDs, count checks and a rollback by ID list; the first wave of our Shopify data section was 406 documents and 159 images in October 2026, and our release scripts dry-run unless told to apply and export the dataset first. Links between our two datasets are stored as validated URL strings, because cross-dataset references need an Enterprise plan. All of it is deploi.ca's own Sanity work.

**What we'd tell you to do first.** Before any schema work, list every Paragraph type and count the nodes that carry a Layout Builder override. Those two numbers decide whether the move is a field mapping or a content redesign, and they belong in the scope before anyone prices the work.

**What we refuse to promise.** We won't promise that revisions, Webform submissions or Views pages come across on their own. Each one is a decision, and the usual answer is to archive it, export it to the service that replaces it, or rebuild it.

## When this answer changes

- Sanity publishes an official Drupal importer or a Drupal course on Sanity Learn
- Sanity Workflows leaves early access
- Sanity adds custom roles or content-type scoping to the Growth plan
- @portabletext/block-tools changes htmlToBlocks() or adds a default table rule
- Content Releases or Media Library changes plan
- Drupal core JSON:API changes how it exposes Paragraphs, Layout Builder or translations

## Ready to plan your Drupal to Sanity migration?

Contact us today to map your Drupal content types, Paragraphs and modules to Sanity before you choose a cutover plan. [Contact us](https://deploi.ca/contact)

## Sources

1. [Drupal.org, JSON:API module](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module), checked 24 October 2026
2. [Drupal.org, Paragraphs](https://www.drupal.org/project/paragraphs), checked 24 October 2026
3. [Drupal.org, Layout Builder](https://www.drupal.org/docs/8/core/modules/layout-builder), checked 24 October 2026
4. [Drupal.org, Pathauto](https://www.drupal.org/project/pathauto), checked 24 October 2026
5. [Drupal.org, Redirect](https://www.drupal.org/project/redirect), checked 24 October 2026
6. [Drupal.org, Content Moderation overview](https://www.drupal.org/docs/8/core/modules/content-moderation/overview), checked 24 October 2026
7. [Drupal.org, Concept: Uses of Views](https://www.drupal.org/docs/user_guide/en/views-concept.html), checked 24 October 2026
8. [Drupal.org, Multisite Drupal](https://www.drupal.org/docs/getting-started/multisite-drupal), checked 24 October 2026
9. [Sanity Docs, Import data](https://www.sanity.io/docs/content-lake/importing-data), checked 24 October 2026
10. [Sanity Learn, Converting HTML to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/converting-html-to-portable-text), checked 24 October 2026
11. [Sanity Docs, Configure the Portable Text Editor](https://www.sanity.io/docs/studio/portable-text-editor-configuration), checked 24 October 2026
12. [Sanity Docs, Managing redirects with Sanity](https://www.sanity.io/docs/developer-guides/managing-redirects-with-sanity), checked 24 October 2026
13. [Sanity Docs, Roles and permissions](https://www.sanity.io/docs/content-lake/roles-concepts), checked 24 October 2026
14. [Sanity, Plans that scale with you](https://www.sanity.io/pricing), checked 24 October 2026
15. [Sanity Docs, Cookbook: Editorial review](https://www.sanity.io/docs/workflows/cookbook-editorial-review), checked 24 October 2026
16. [Sanity Docs, Scheduled drafts](https://www.sanity.io/docs/studio/scheduled-drafts), checked 24 October 2026
17. [Sanity Docs, Media Library introduction](https://www.sanity.io/docs/media-library/introduction), checked 24 October 2026
18. [Sanity Docs, Forms with Sanity](https://www.sanity.io/docs/developer-guides/forms-with-sanity), checked 24 October 2026
19. [Sanity Docs, Search text content with GROQ](https://www.sanity.io/docs/content-lake/search-content-with-groq), checked 24 October 2026
20. [Sanity Docs, Important considerations for schema and content migrations](https://www.sanity.io/docs/content-lake/important-considerations-for-schema-and-content-migrations), checked 24 October 2026
21. [Sanity, The Drupal developer's introduction to Sanity.io](https://www.sanity.io/blog/sanity-vs-drupal7), checked 24 October 2026
22. [Sanity, You'll need a CMS eventually. Let your agent set it up.](https://www.sanity.io/blog/sanity-remote-mcp-server-is-generally-available), checked 24 October 2026
23. [Sanity Learn, Migrating content from WordPress to Sanity: Introduction](https://www.sanity.io/learn/course/migrating-content-from-wordpress-to-sanity/introduction-to-wp-migration), checked 24 October 2026
24. [Sanity Docs, Content Releases user guide](https://www.sanity.io/docs/user-guides/content-releases), checked 24 October 2026
25. [Sanity Docs, String](https://www.sanity.io/docs/studio/string-type), checked 24 October 2026
26. [Sanity Docs, How to implement front-end search with Sanity](https://www.sanity.io/docs/developer-guides/how-to-implement-front-end-search-with-sanity), checked 24 October 2026
27. [Sanity Learn, Replatforming from a legacy CMS to a Content Operation System](https://www.sanity.io/learn/track/replatforming-to-sanity), checked 24 October 2026
28. [Sanity Docs, Cross-dataset references](https://www.sanity.io/docs/studio/cross-dataset-references), checked 24 October 2026
29. [Sanity Docs, How early access works](https://www.sanity.io/docs/workflows/prerelease), checked 24 October 2026
30. [Sanity Learn, Uploading assets efficiently](https://www.sanity.io/learn/course/refactoring-content/uploading-assets-efficiently), checked 24 October 2026
31. [Sanity blog, August releases](https://www.sanity.io/blog/august-releases-2026), checked 24 October 2026
32. [Sanity on GitHub, sanity-migration skill](https://github.com/sanity-io/agent-toolkit/blob/main/skills/sanity-migration/SKILL.md), checked 25 October 2026
33. [Drupal.org, Drupal 8.7.0 release notes](https://www.drupal.org/project/drupal/releases/8.7.0), checked 25 October 2026
