🇹🇷 Türkçe: Bu yazının Türkçesini oku →
I have been moving content from one place to another for thirty years. CD-ROMs, Flash, enterprise content management systems that cost more than a house, and eventually the web. The format keeps changing. The job never does: get the content to the right place, keep it accurate, keep it alive.
So when someone tells me they want to move off Sanity and onto WordPress, I do not react with surprise or with a sales pitch. I ask them the same question I have been asking since 1996, when I was shipping a database on a plastic disc and could not update it: where does this content actually need to go, and who has to be able to change it without opening a ticket?
That question decides everything. This article is how I think about a Sanity-to-WordPress migration — first in plain terms for anyone weighing the decision, then, near the end, in the technical detail that actually determines whether the migration succeeds or quietly rots.
First, let me be fair to Sanity
I am not here to tell you Sanity is bad. It is a genuinely well-built headless CMS. Structured content, a real query language in GROQ, content modelled as data rather than as pages, real-time collaboration, and an editing experience that developers respect. If you are feeding six screens from one content source — a website, a mobile app, in-store displays, digital price labels, a kiosk, a partner API — headless is the correct architecture and Sanity is a serious choice. I would not talk you out of it.
I wanted headless before it had a name. In the nineties I built a university preference assistant and shipped it on a newspaper-insert CD-ROM. When a quota changed, the disc was already in a hundred thousand homes with the old number frozen inside it. I wanted content in one place, delivered to whatever screen needed it, updatable without touching the delivery layer. I wanted Sanity in 1996 and could not build it, because the infrastructure did not exist.
So I am the last person to sneer at the idea.
Why teams actually leave Sanity for WordPress
Here is the uncomfortable truth I have watched play out again and again: most organisations running a headless CMS are feeding exactly one channel — a website. They bought a distribution system to deliver content to their own front door.
And they are paying the full price of that architecture. The content team cannot add a field without a developer changing the schema code and deploying it. Every editorial tweak that touches structure becomes a pull request. Preview is a project of its own. The people who write the content are permanently dependent on the people who ship the code. That is a fine trade when you are feeding many channels. It is a bad joke when you are feeding one website.
So teams migrate to WordPress for reasons that are rarely technical:
- Editorial independence. The marketing team wants to add a field, reorder a page, publish a landing page for a campaign on Friday afternoon, without waiting on an engineering sprint.
- Cost and dependency. Sanity’s pricing scales with usage and seats, and every content change carries a developer cost. For a single-channel site, that math stops making sense.
- The ecosystem. WooCommerce, Yoast or RankMath, forms, memberships, an enormous plugin economy, and a hiring pool of people who already know the tool.
- Longevity. This is the one I care about most, and I will come back to it.
Let me be blunt about that last point, because it is the thing I actually believe. Platforms rarely die of technical failure. They die of ownership. I watched Vignette, Interwoven, FatWire, Stellent — every serious ECM platform of the early 2000s — get acquired, absorbed, renamed, and quietly retired. Not one of them died because the software was bad. They died because someone bought the company and your content strategy became a line item in an integration plan you were never consulted about.
Sanity is a well-funded private company. I am not predicting its demise. But when I choose a platform for a client, I ask: who can sell this company, and what happens to my content the day after? WordPress cannot be acquired. Nobody owns it — not even Automattic. That is not a small feature. For a lot of teams, that alone is the whole argument.
How I plan the migration — the part most people skip
The single biggest mistake in any migration is treating it as an export-import problem. It is not. It is a content-modelling problem wearing an export-import costume.
I come from print, and print taught me something digital people rarely learn: structure before style, always. So before I touch a single line of migration code, I do this:
1. Inventory the content model. Every Sanity document type, every field, every reference, every custom object inside your Portable Text. Not a vague list — an actual map. I want to see the shape of the thing before I move it.
2. Decide what not to migrate. This is the step everyone resists and everyone regrets skipping. A content model that grew over three years is full of fields nobody uses, document types that were created for a campaign that ended, and structure that exists because a developer was being thorough, not because an editor needed it. Migration is the one moment you are allowed to throw things away. Take it. Every idea I ever dismissed as too simple turned out to sell; every over-engineered structure I ever built turned out to be dead weight. Prune ruthlessly.
3. Map Sanity’s model to WordPress’s model — honestly. These two systems think differently. Sanity is fully structured content as data. WordPress is a post with metadata. Fighting that difference is how migrations die. I will get into the specifics below, but the planning principle is: map each Sanity document type to a WordPress post type or page, each field to a native field, an ACF field, or a taxonomy, and be willing to flatten structure that does not earn its keep.
4. Build the target in WordPress first — empty. Custom post types, ACF field groups, taxonomies, the block patterns your editors will actually use. Get the shape right and validated by the people who will live in it, before a single row of real content arrives.
5. Write the transform, then dry-run it on staging. Repeatedly. Never migrate straight to production. Never. I burned CD-ROMs for a living and I can tell you the worst feeling in this business is a mistake you cannot recall. A staging dry run is your recall button. Use it until the output is boring.
6. Redirects and SEO before cutover, not after. Your URL structure will change. If you do not map old paths to new ones, you will watch rankings you spent years building evaporate in a week.
7. Cut over, then watch. Migration does not end at cutover. It ends when the content team has published three real things without calling you.
A word on timing, because it matters more than the tooling
I sold the rights to my CD-ROM tourism titles the same night they were offered, because I could see the format was ending. Knowing when to move is worth more than being excellent at the thing you are moving away from.
The same applies here. Do not migrate off Sanity because headless is unfashionable this quarter, and do not stay on it because migrating feels like admitting a mistake. Count your channels. If the honest answer is “one website,” the sophisticated architecture is a machine solving a problem you do not have, and moving to WordPress is not a downgrade — it is right-sizing. If the honest answer is “six screens and growing,” stay where you are and stop reading.
The Technical Part: Where a Sanity Migration Actually Breaks
Everything above is the strategy. Here is the part where the migration succeeds or fails on details. If you are the developer doing the work, this is the section to print out. These are the traps I have watched swallow migrations whole.
Portable Text is the whole battle
Sanity does not store rich text as HTML. It stores it as Portable Text — an array of block objects, each with children (spans carrying marks), a markDefs array for annotations like links, a style (normal, h2, blockquote), and, crucially, arbitrary custom _type objects embedded inline: images, code blocks, callouts, embeds, product references.
WordPress stores rich text as Gutenberg block markup — HTML with block-delimiter comments. These two formats do not map automatically, and the gap between them is where fidelity dies.
You have two honest options:
- Serialize Portable Text to HTML (with a library like
@portabletext/to-html) and drop it into acore/freeformorcore/htmlblock. Fast, but you lose the block-native editing experience — your content arrives as one opaque HTML lump. - Map block-by-block to native Gutenberg blocks — Portable Text
blockwith stylenormaltocore/paragraph,h2tocore/heading,blockquotetocore/quote, list items tocore/list, embedded image objects tocore/image, code objects tocore/code. Far more work, and the correct answer if editors will keep touching this content.
The thing that will actually bite you is custom marks and embedded objects. Every project has them — a highlight mark, an inline product reference, a callout object, an accordion. There is no automatic translation for these. You must decide, for each one, what it becomes in WordPress, and write the handler. Skip one and it either vanishes silently or dumps as raw JSON into your published post. Inventory every _type in your Portable Text before you write a single serializer.
Image hotspot and crop metadata will be lost
Sanity images are not just files. Each image reference carries hotspot and crop metadata, and Sanity’s image CDN applies them on the fly per-request. WordPress has no equivalent concept. When you download originals and push them into the media library, the hotspot and crop data is gone. Your carefully art-directed crops revert to whatever the theme does by default.
Plan for this explicitly. Either accept the loss and re-crop the images that matter, or bake the crops into the exported files so at least the primary rendition is correct. Do not discover this after cutover when the client asks why every hero image is centred wrong.
The draft overlay and the drafts. prefix
Sanity stores unpublished edits as separate documents with a drafts. prefix on the _id. A document you are editing exists twice: drafts.abc123 (your working copy) and abc123 (the published version). If your export naively grabs everything, you will import both — and get duplicate posts, one of them an unfinished draft masquerading as published content.
Decide your rule up front: migrate published only, or migrate drafts as WordPress drafts. Then filter on the _id prefix during the transform. This is a two-line fix that causes a two-day cleanup if you miss it.
References, slugs, and the two-pass problem
Sanity content is relational — documents reference other documents by _ref. An article references its author; a product references its category. When you import into WordPress, those references point to Sanity _ids that mean nothing to WordPress.
You cannot resolve references in a single pass, because the target may not exist yet when you import the source. Do it in two passes: first import every document and build a map of Sanity _id to new WordPress post ID; then, in a second pass, rewrite every reference using that map into ACF relationship fields, post-to-post connections, or taxonomy terms. Preserve _id and _type on each imported item (in post meta) until the second pass finishes. Get the ordering wrong and your relationships silently point at the wrong posts.
Slugs need the same care. Preserve Sanity slugs where you can so your redirect map stays sane, and watch for collisions — Sanity slugs are unique per document type, but WordPress slugs may collide across types.
The export mechanics
Use sanity dataset export <dataset> <file.tar.gz>. It produces an NDJSON file of every document plus an assets bundle (or CDN-resolvable asset references). Work from that file, not from live GROQ queries against production — you want a frozen snapshot you can re-run your transform against a hundred times without hammering the API or getting a moving target. That frozen snapshot is your forty-minute CD burn done right: deliberate, repeatable, and safe to fail against.
The frontend contract changes
If a Next.js or Nuxt frontend is currently pulling from Sanity via GROQ, that frontend does not survive the migration untouched. It now needs to consume the WordPress REST API or WPGraphQL, and the shape of the data is completely different. GROQ projections and WordPress’s endpoints are not interchangeable. Budget for rebuilding the data layer of the frontend, or for retiring the custom frontend entirely and using a WordPress theme. This is often the largest hidden cost, and the one clients are most surprised by. Surface it in the first meeting, not the last.
Localization
If you used Sanity’s internationalization — whether document-level or field-level — you have to decide the WordPress equivalent before you migrate, not after. Multisite, Polylang, or WPML each model translations differently, and the transform that splits Sanity’s locale structure into your chosen approach is not trivial. I have run this on a five-language multisite and I can tell you: multilingual is not a translation problem, it is an architecture problem. Decide the architecture first. German runs long, Turkish runs longer, Arabic runs the other direction — if your target model assumes a language, you have already failed.
My honest closing position
Migrating from Sanity to WordPress is not a step down the sophistication ladder. For a single-channel site, it is the correct correction — trading an architecture built for many screens for a platform built to let ordinary people publish without asking permission.
But do the modelling work. The reason my own CMS failed twenty years ago was not a bad export routine. It was that I never carried the past forward properly — I built a system with no backward compatibility, and eventually I abandoned my own users because I could not carry them. WordPress survives precisely because it never does that. Do not undo that virtue with a careless migration.
Map the model. Prune what you do not need. Handle every Portable Text type by hand. Do the references in two passes. Dry-run on staging until it is boring. Set your redirects before cutover.
Do that, and the content lands somewhere ordinary people can keep it accurate and keep it alive — which, as I keep telling anyone who will listen, is the entire job.
One More Thing: “But Can’t WordPress Just Do What Sanity Does?”
Here is the question I get the moment someone decides to move: if we leave Sanity, do we have to give up the headless model we actually liked? No. You don’t. And this is where people who still think of WordPress as “just a blog” get a genuine surprise.
WordPress is not new to this. The REST API has shipped in core since late 2016 — nearly a decade of production hardening. WPGraphQL has been around almost as long, and it is now backed by Automattic and on its way to canonical status as WordPress’s GraphQL layer. In other words, the exact thing you pay Sanity for — structured content delivered as JSON to whatever front-end asks — WordPress has been doing, in the open, for years. It did not lose the headless argument. It absorbed it.
So you have two honest paths off Sanity, not one:
- Traditional WordPress — the rest of this article. Give the editors their theme back and let them publish without tickets.
- Headless WordPress — keep the decoupled architecture you liked; just swap the backend. WordPress with WooCommerce, ACF and custom post types as the content store, WPGraphQL (or the REST API) as the query layer, and your existing Next.js / Nuxt / Astro front-end — or Faust.js if you want a WPGraphQL-native framework — sitting on top, deployed on Vercel exactly as before.
Model your content with custom post types and ACF (or native blocks), query it with WPGraphQL, and your front-end barely notices the CMS changed underneath it. You keep every advantage of headless. And here is the part that should make you sit up: you gain things a pure headless SaaS never gives you.
- A real commerce engine. WooCommerce. Try bolting a mature store onto a headless-only CMS and watch the custom code and the bill pile up.
- Editorial power for humans, not just developers — a block editor, media library, revisions, roles, and an ecosystem of plugins for SEO, forms, memberships and more, none of which you rebuild from scratch.
- No per-seat pricing, no usage meter. Add a hundred editors; the bill doesn’t move.
- Ownership. The whole argument of this series — nobody can buy WordPress out from under you.
Some of what WordPress does here, Sanity simply doesn’t — commerce, the plugin economy, self-hosting with no metered API. Sanity is excellent at what it does; I said so at the top and I meant it. But “we want structured content over an API” is not a reason to leave WordPress behind — it is a reason to run WordPress headless. Add the headless layer, keep everything WordPress already does well, and you are not trading down. You are getting the model you wanted, with a much bigger toolbox behind it.
That’s the surprise. You didn’t need to choose between headless and WordPress. You can have both.
Last modified: July 19, 2026
United States / English
Slovensko / Slovenčina
Canada / Français
Türkiye / Türkçe