Sanity Migration Playbook: Moving from WordPress, Contentful, or Drupal
I've migrated somewhere around 40 projects to Sanity over the past three years. Some were clean two-week sprints. Others turned into three-month slogs that made me question my career choices. The difference almost never comes down to the source CMS — it comes down to preparation, content modeling decisions, and being honest about what you're actually signing up for.
This is the guide I wish I'd had when I started doing CMS migrations. It covers moving from WordPress, Contentful, and Drupal to Sanity GROQ-powered world. I'm going to be blunt about where Sanity shines, where it'll frustrate you, and what the real timelines look like.
Table of Contents
- Why Teams Are Moving to Sanity in 2025
- Pre-Migration Audit: The Step Everyone Skips
- WordPress to Sanity Migration
- Contentful to Sanity Migration
- Drupal to Sanity Migration
- Content Modeling: Getting Your Schemas Right
- Data Migration Strategies and Tooling
- The Hidden Costs Nobody Talks About
- Post-Migration Checklist
- Timeline and Budget Comparison
- FAQ

Why Teams Are Moving to Sanity in 2025
Let's get the obvious stuff out of the way. Sanity's real-time collaborative editing, customizable Studio, and structured content approach are genuinely good. But the reason most teams reach out to us about migration isn't because they read a blog post about Sanity's features. It's because something broke.
WordPress sites hit scaling walls around 50,000+ pieces of content with complex custom post types. Contentful's pricing model starts squeezing at the enterprise tier — we've seen teams facing $3,500+/month bills for what amounts to a content API. Drupal teams can't find developers anymore, at least not ones who want to work with PHP templating in 2025.
Sanity's pricing model is genuinely more predictable for most teams. The free tier covers up to 100K API requests/month and 500K assets. The Growth plan at $99/month/project gets you 2.5M API requests and 1M assets. For comparison, Contentful's Team plan runs $300/month and Contentful's Premium tier can easily exceed $2,000/month.
But here's my honest take: if your current CMS is working fine and your team is productive, don't migrate just because Sanity is newer or cooler. Migrations always cost more than you think.
Pre-Migration Audit: The Step Everyone Skips
Before you write a single line of migration code, you need a content audit. Not a quick scan — an actual audit. Here's what that looks like:
Content Inventory
Document every content type, every field, every relationship. I use a spreadsheet with these columns:
- Content type name
- Total items
- Fields (with types)
- Relationships to other content types
- Media attachments (count and total size)
- Custom functionality (shortcodes, widgets, embeds)
- Last modified date
- Still relevant? (Yes/No/Maybe)
You'll be shocked how much content is dead weight. On one WordPress migration, a client had 12,000 posts. After the audit, only 4,200 were still relevant. That's 65% less content to migrate, test, and validate.
Technical Dependency Mapping
List every plugin, module, or integration your current CMS uses. For each one, determine:
- Can Sanity handle this natively?
- Is there a Sanity plugin for it?
- Do we need to build a custom solution?
- Can we drop this entirely?
This mapping alone will save you weeks of surprises down the road.
Team Readiness Assessment
Sanity Studio is React-based. Your content editors will need training. Your developers will need to learn GROQ (or use GraphQL, though GROQ is where Sanity really sings). Budget 1-2 weeks for team onboarding — not as a nice-to-have, but as a line item.
WordPress to Sanity Migration
WordPress is the most common source CMS we migrate from. It's also the trickiest, because WordPress is not just a CMS — it's an entire application platform that people have bolted everything onto.
What Transfers Cleanly
- Posts and pages (basic content)
- Categories and tags
- Featured images
- Author data
- Basic custom fields (ACF, Meta Box)
What Gets Messy
- Gutenberg blocks: Each block type needs a corresponding Sanity Portable Text custom block or object type. If you've got 15+ custom Gutenberg blocks, budget significant time here.
- Shortcodes: These need to be parsed, interpreted, and converted to Portable Text annotations or custom blocks. WPBakery and Elementor shortcodes are particularly painful.
- Plugin-generated content: WooCommerce products, Yoast SEO data, ACF repeater fields — each requires custom migration logic.
- Media library: WordPress stores multiple image sizes. Sanity handles image transformations on-the-fly, so you only need the originals. But finding the originals in a messy wp-uploads folder? Fun times.
Migration Script Approach
I typically use a Node.js script that hits the WordPress REST API and writes to Sanity's mutation API:
import { createClient } from '@sanity/client'
import fetch from 'node-fetch'
const sanity = createClient({
projectId: 'your-project-id',
dataset: 'production',
token: process.env.SANITY_WRITE_TOKEN,
apiVersion: '2025-01-01',
useCdn: false,
})
const WP_API = 'https://yoursite.com/wp-json/wp/v2'
async function migratePosts(page = 1) {
const res = await fetch(`${WP_API}/posts?per_page=100&page=${page}`)
const posts = await res.json()
const totalPages = res.headers.get('x-wp-totalpages')
const transaction = sanity.transaction()
for (const post of posts) {
transaction.createOrReplace({
_id: `wp-post-${post.id}`,
_type: 'post',
title: post.title.rendered,
slug: { current: post.slug },
publishedAt: post.date,
// Body requires HTML-to-Portable-Text conversion
body: await convertToPortableText(post.content.rendered),
})
}
await transaction.commit()
console.log(`Migrated page ${page} of ${totalPages}`)
if (page < totalPages) {
await migratePosts(page + 1)
}
}
The convertToPortableText function is where 80% of the complexity lives. I use the @sanity/block-tools package combined with jsdom for HTML parsing. It handles basic HTML well, but custom elements and shortcodes need individual handlers.
Realistic Timeline
For a typical WordPress site with 500-2,000 posts, standard custom fields, and a handful of custom post types: 4-8 weeks including content modeling, migration scripting, validation, and editor training.

Contentful to Sanity Migration
Contentful-to-Sanity is actually the smoothest migration path of the three. Why? Because both are structured content platforms with similar mental models. Your content is already in a headless CMS with defined content types and fields.
Key Differences to Account For
| Feature | Contentful | Sanity |
|---|---|---|
| Rich text | Rich Text (JSON-based) | Portable Text (JSON-based) |
| Content modeling | Web UI | Code-defined schemas |
| Query language | GraphQL / REST | GROQ (+ GraphQL) |
| Localization | Built-in field-level | Plugin or custom |
| References | Links (Entry/Asset) | References with types |
| Webhooks | Yes | Yes |
| Asset handling | Built-in CDN | Sanity CDN + hotspot/crop |
| Pricing (mid-tier) | ~$300/mo (Team) | $99/mo (Growth) |
Rich Text Conversion
Contentful's Rich Text and Sanity's Portable Text are both JSON-based, which is great. But they have different structures. You'll need to write a transformer:
function contentfulRichTextToPortableText(richTextField) {
return richTextField.content.map(node => {
switch (node.nodeType) {
case 'paragraph':
return {
_type: 'block',
style: 'normal',
children: node.content.map(mapInlineContent),
}
case 'heading-2':
return {
_type: 'block',
style: 'h2',
children: node.content.map(mapInlineContent),
}
case 'embedded-entry-block':
// Map to your custom Portable Text type
return mapEmbeddedEntry(node)
// ... handle all node types
}
}).filter(Boolean)
}
Content Type to Schema Mapping
Contentful content types map fairly directly to Sanity document and object types. The biggest shift is that Sanity schemas are defined in code (JavaScript/TypeScript), not in a web UI. This is actually a huge advantage — your content model lives in version control.
Use the Contentful Management API to export your content model, then write a script that generates Sanity schema files:
contentful space export --space-id YOUR_SPACE_ID --export-dir ./export
Realistic Timeline
For a Contentful space with 10-20 content types and 5,000-10,000 entries: 3-5 weeks. It's faster because you're already thinking in structured content.
Drupal to Sanity Migration
Drupal migrations are the ones that make me pour a second coffee. Not because Drupal is bad — it's a powerful system. But Drupal sites tend to be old, customized to the hilt, and running on infrastructure that nobody fully understands anymore.
The Drupal-Specific Challenges
- Content types with 50+ fields: Drupal makes it easy to add fields. Too easy. I've seen content types with 80 fields, half of which are unused.
- Taxonomy term references: Drupal's taxonomy system is flexible but can create deeply nested hierarchies that need flattening for Sanity.
- Paragraphs module: If the site uses Drupal Paragraphs (and most modern Drupal sites do), each paragraph type becomes a Portable Text block type or Sanity object. This is the biggest single task.
- Media entities: Drupal 9/10's media system is more complex than WordPress's. Multiple media types, reusable media entities, and file field configurations all need mapping.
- Multilingual content: Drupal's translation system is sophisticated. Sanity doesn't have built-in localization at the same level — you'll need the
@sanity/document-internationalizationplugin or a field-level approach.
Migration Approach
I prefer using Drupal's JSON:API module (included in Drupal core since 9.x) as the extraction layer:
async function fetchDrupalContent(type, page = 0) {
const limit = 50
const offset = page * limit
const url = `${DRUPAL_URL}/jsonapi/node/${type}?page[limit]=${limit}&page[offset]=${offset}&include=field_image,field_paragraphs`
const res = await fetch(url, {
headers: { Authorization: `Basic ${DRUPAL_AUTH}` },
})
return res.json()
}
For older Drupal 7 sites without JSON:API, you might need to query the database directly. Drupal 7's database schema is... an experience. The field_data_* tables will haunt your dreams.
Realistic Timeline
Drupal migrations vary wildly. A straightforward Drupal 10 site with 5-10 content types: 5-8 weeks. A legacy Drupal 7 site with 30+ content types, Paragraphs, and multilingual content: 8-16 weeks. I'm not exaggerating.
Content Modeling: Getting Your Schemas Right
Here's the thing most migration guides won't tell you: don't replicate your old content model in Sanity. This is your chance to fix years of accumulated content debt.
Common Modeling Mistakes
- Creating a 1:1 field mapping: Just because WordPress had a "subtitle" custom field doesn't mean Sanity needs one. Maybe it should be part of a structured "hero" object.
- Over-nesting objects: Sanity lets you nest objects deeply. Resist the urge. Flat-ish schemas are easier to query with GROQ and easier for editors to work with.
- Ignoring Portable Text's power: Don't just dump HTML into a single text field. Design custom block types that match your content patterns. A "callout" block, a "code snippet" block, an "image with caption" block — these make editors' lives better.
Schema Design Process
I follow this order:
- Audit existing content (done in pre-migration)
- Identify the actual content patterns (not what the CMS imposed)
- Design schemas on paper/whiteboard first
- Build schemas in code
- Import a small test batch (50-100 items)
- Have editors test the Studio experience
- Iterate on schemas before full migration
Steps 5-7 are critical and often skipped. We've written more about content modeling approaches in our headless CMS development work.
Data Migration Strategies and Tooling
Essential Tools
@sanity/client: The official JavaScript client for reading/writing Sanity data@sanity/block-tools: Converts HTML to Portable Textsanity dataset import/export: CLI tools for full dataset operationsndjson: Sanity uses newline-delimited JSON for imports — get comfortable with itjsdomorhtmlparser2: For HTML parsing during rich text conversion
Migration Architecture
I structure every migration as a pipeline with four stages:
Extract → Transform → Load → Validate
Each stage is a separate script. This matters because you will run the migration multiple times — typically 5-10 times before the final production run. Having separate stages means you can re-run just the parts that need fixing.
# Extract
node scripts/extract-wordpress.js > data/raw-posts.ndjson
# Transform
node scripts/transform-posts.js < data/raw-posts.ndjson > data/sanity-posts.ndjson
# Load
sanity dataset import data/sanity-posts.ndjson production --replace
# Validate
node scripts/validate-migration.js
Handling Assets
Images and files are always the slowest part. Sanity's asset pipeline is good, but uploading 10,000 images takes time. Tips:
- Upload assets first, maintain a mapping of old URLs to new Sanity asset IDs
- Use concurrent uploads (but respect rate limits — 25 req/sec for the Growth plan)
- Verify image dimensions and formats before upload
- Don't migrate thumbnail sizes — Sanity generates these on the fly via its image CDN
The Hidden Costs Nobody Talks About
Let me be straight with you about costs that don't show up in the typical migration estimate.
URL Redirects
If you're changing your frontend (which you probably are if you're moving to a headless CMS), you need redirect mapping. Every. Single. URL. For SEO, this is non-negotiable. A site with 5,000 pages needs 5,000 redirect rules. Tools like next.config.js redirects or Vercel's _redirects file can handle this, but someone has to build the mapping.
SEO Metadata Migration
Yoast SEO data from WordPress. Metatag module data from Drupal. Contentful's SEO fields. All of this needs to come over. Custom meta titles, descriptions, Open Graph images, canonical URLs, structured data — it's a project within the project.
Editor Training and Documentation
Budget 2-4 days minimum. Sanity Studio is intuitive, but it's different from what your editors know. We typically create custom Studio documentation with screenshots and record 3-5 walkthrough videos.
Frontend Development
This is the elephant in the room. Migrating content to Sanity is only half the project. You also need a frontend that consumes the content. Whether you're using Next.js, Astro, or another framework, the frontend build is often 50-70% of the total project cost. Check out our work with Next.js and Astro if you're evaluating frontend options.
Timeline and Budget Comparison
Based on projects we've completed in 2024-2025:
| Migration Path | Content Volume | Complexity | Timeline | Budget Range |
|---|---|---|---|---|
| WordPress → Sanity | < 1,000 pages | Low | 3-5 weeks | $8K-$15K |
| WordPress → Sanity | 1,000-10,000 pages | Medium | 6-10 weeks | $15K-$35K |
| WordPress → Sanity | 10,000+ pages | High | 10-16 weeks | $35K-$75K |
| Contentful → Sanity | < 5,000 entries | Low-Medium | 3-5 weeks | $7K-$18K |
| Contentful → Sanity | 5,000-20,000 entries | Medium | 5-8 weeks | $18K-$40K |
| Drupal → Sanity | < 2,000 nodes | Medium | 5-8 weeks | $12K-$25K |
| Drupal → Sanity | 2,000-15,000 nodes | High | 8-14 weeks | $25K-$60K |
| Drupal 7 → Sanity | Any | Very High | 10-20 weeks | $35K-$90K |
Note: These ranges include content migration only. Frontend development is additional. Contact us at /pricing for project-specific estimates.
These numbers include content modeling, migration scripting, data validation, and basic editor training. They don't include frontend development, design, or ongoing maintenance.
Post-Migration Checklist
Here's the checklist I use on every migration:
- All content types migrated and verified
- All references/relationships intact
- All images and files uploaded and linked correctly
- Rich text content renders correctly (check for broken formatting)
- URL redirects in place and tested
- SEO metadata migrated (titles, descriptions, OG data)
- XML sitemap regenerated
- Search console updated with new sitemap
- Analytics tracking preserved
- Editor accounts created and permissions set
- Editor training completed
- Content preview (draft mode) working
- Webhooks configured for build triggers
- Backup of source CMS data archived
- DNS changes planned (if applicable)
- Performance baseline measured
- 404 monitoring set up for first 30 days
That last point is important. No matter how thorough your redirect mapping, some URLs will slip through. Monitor your 404s aggressively for the first month.
FAQ
How long does a typical WordPress to Sanity migration take? For a standard WordPress site with under 2,000 posts and straightforward custom fields, expect 4-8 weeks. That includes content modeling, migration scripting, data validation, and editor training. Sites with complex Gutenberg blocks, WooCommerce, or multilingual content can take 10-16 weeks. The content volume matters less than the complexity of your content types.
Can I migrate from Contentful to Sanity without losing data? Yes, and it's actually the cleanest migration path among the three I cover here. Both platforms use structured, JSON-based content so the mapping is relatively direct. Rich text needs conversion from Contentful's format to Portable Text, and references need remapping, but you shouldn't lose any data. I recommend running the migration against a staging dataset first and doing a thorough content comparison before cutting over.
What happens to my SEO rankings during a CMS migration? This is the question that keeps marketing directors up at night. If you handle redirects properly, maintain your URL structure where possible, and migrate all SEO metadata, you should see minimal impact. Google's own documentation says that properly redirected migrations may see a temporary dip of 2-4 weeks before recovering. The key word is "properly." Skip the redirect mapping and you'll tank your rankings. We've seen it happen.
Is Sanity cheaper than Contentful for enterprise use? In most cases, yes — sometimes significantly. Sanity's Growth plan at $99/month covers what you'd need Contentful's $300/month Team plan for. At enterprise scale, the difference gets larger. Contentful's Premium pricing isn't publicly listed but typically runs $2,000-$4,000+/month. Sanity Enterprise pricing is also custom but generally comes in lower for equivalent usage. The real cost difference is in API calls — Sanity's included limits are more generous.
Should I migrate my Drupal 7 site to Sanity or upgrade to Drupal 10 first? Go directly to Sanity. Migrating from Drupal 7 to Drupal 10 is nearly as much work as migrating to a different CMS entirely — the architecture changed that dramatically between versions. If you're already investing in a major migration, you might as well move to the platform you actually want to be on long-term. The one exception: if your team is deeply invested in the Drupal ecosystem and just wants to modernize, Drupal 10 with a headless frontend is a valid path.
Do I need to rebuild my frontend when migrating to Sanity? If you're coming from a monolithic CMS like WordPress or Drupal, yes — you'll need a new frontend since Sanity is headless. This is usually the bigger part of the project. If you're coming from Contentful, you can often reuse your existing frontend with modified API calls, especially if you're already using Next.js or a similar framework. We handle both the CMS migration and frontend builds as integrated projects.
Can I run my old CMS and Sanity in parallel during migration? Absolutely, and I recommend it. Run both systems for 2-4 weeks after your initial migration. Editors can continue working in the old CMS while you validate data in Sanity. Just freeze content in the old system before your final migration run — you don't want to chase a moving target. Some teams set up a "content freeze" date 48 hours before the final cutover.
What's the biggest mistake teams make during a Sanity migration? Trying to replicate their old CMS structure exactly in Sanity. I see this constantly. Teams come from WordPress and try to build WordPress-shaped schemas in Sanity — generic "page" types with flexible layouts instead of purpose-built content types. Sanity's strength is structured content. Use the migration as an opportunity to model your content properly. Spend an extra week on content modeling. It'll save you months of pain later. If you want guidance on this, reach out to us — content modeling is one of the things we spend the most time on in migration projects.