If you're running your blog on WordPress but building your frontend with modern stacks like Next.js, Remix, or Astro, Sanity can be a much better home for your content. Structured content, Portable Text, powerful querying, real-time collaboration… all the good stuff.
The problem: actually moving your WordPress content into Sanity is usually the painful bit.
Most guides show you how to write your own migration script from scratch. We did that work, battle-tested it on real blogs, and then open-sourced the tool so you don’t have to reinvent the wheel:
👉 Repo: https://github.com/salttechno/wp-to-sanity-migration
This post walks you through what it does and how to use it.
What this tool does for you
The WordPress to Sanity Blog Migration Tool is a Node.js-based CLI that talks to the WordPress REST API and pushes everything into Sanity in a structured way.
Out of the box, it:
- ✅ Migrates posts, categories, tags, and media (images)
- ✅ Converts WordPress HTML content to Portable Text for richer editing and rendering
- ✅ Preserves relationships (categories, tags, featured images)
- ✅ Uploads images into Sanity’s asset pipeline
- ✅ Is idempotent – safe to run multiple times (no duplicate docs)
- ✅ Ships with a ready-to-use Sanity Studio configured for the migrated content
It’s focused on blog migration first, but you can extend it to pages, custom post types, authors, etc.
Prerequisites
You’ll need:
- Node.js 18+
- A WordPress site with the REST API enabled (default for WP 4.7+)
- A Sanity project (free tier is enough)
- A Sanity API token with write access
If you can open:
https://your-site.com/wp-json/wp/v2/posts
in your browser, you’re good on the WordPress side.
Quick start: migrate your blog in a few steps
1. Clone the repo and install dependencies
git clone https://github.com/salttechno/wp-to-sanity-migration.git
cd wp-to-sanity-migration
npm install
2. Configure your .env
Copy the example env file:
cp .env.example .env
Then edit .env:
# WordPress Configuration
WP_API_URL=https://your-wordpress-site.com/wp-json/wp/v2
# Sanity Configuration
SANITY_PROJECT_ID=your-project-id
SANITY_DATASET=production
SANITY_TOKEN=your-write-token
SANITY_API_VERSION=2024-11-25
Tip: you’ll find your Project ID and can generate a token in
sanity.io/manageunder your project’s settings.
3. Test connections
Before importing anything, verify both sides are reachable:
npm run test
This checks that:
- WordPress REST API is reachable
- Sanity credentials and token are valid
4. Run the migration
Now the fun part:
npm run migrate
Behind the scenes, the script:
- Fetches categories
- Fetches tags
- Fetches media (images) and uploads them to Sanity
- Fetches posts and creates/updates corresponding Sanity documents, wiring up all relationships
The migration is idempotent, so you can safely re-run it while tweaking schemas or content mapping.
Browse your content in Sanity Studio
The repo also includes a preconfigured Sanity Studio in the the studio/ folder.
From the project root:
cd studio
npm install
cp .env.example .env # add your Sanity project details
npm run dev
Open:
http://localhost:3333
You’ll see:
- A Post schema with Portable Text fields
- Categories and Tags linked via references
- Featured images wired up as Sanity image assets
You can deploy the Studio as well:
cd studio
npm run deploy
Sanity will give you a hosted Studio URL like your-blog.sanity.studio.
How the migration works under the hood
If you’re curious (or planning to extend it), here’s the rough architecture:
wordpress-client.js
Talks to the WordPress REST API, fetches posts, categories, tags, and media.sanity-client.js
Uses@sanity/clientto create or replace documents in Sanity.html-to-blocks.js
Converts HTML from WordPress into Portable Text blocks, so your editors aren’t stuck editing raw HTML.transformers.js
Maps WordPress entities into Sanity document shapes: posts →post, terms →category/tag, media →imageassets.migrate.js
Orchestrates the whole process in the right order and ensures it’s safe to re-run.
This makes it easy to:
- Add new content types (e.g. case studies)
- Plug in custom fields
- Change schema names or structures on the Sanity side
What gets migrated (and what doesn’t)
Out of the box, the tool handles:
✅ Posts
- Title, slug, published date
- Body content → Portable Text
- Excerpt → Portable Text
- Featured image
- Categories & tags
✅ Categories & Tags
- All your post categories and tags, with relationships preserved
✅ Media (images)
- Images referenced from posts are uploaded into Sanity as proper image assets
By default, it does not migrate:
- Pages
- Authors
- Comments
- Custom post types
- WordPress-specific metadata
The point is to give you a clean, production-ready baseline for blog content, not an opaque black box that tries to handle every plugin and edge case.
Extending it: pages, authors, custom post types
You can absolutely extend this for more advanced setups.
Some ideas:
Pages
- Add a
pageschema tostudio/schemas/ - Add a new fetch function in
wordpress-client.jsforpages - Create a page transformer in
transformers.js - Wire it into
migrate.js
Authors
- Fetch authors from the WordPress API
- Add an
authorschema (already included as an optional type) - Link authors in the post transformer
Custom post types
- Create additional WordPress client functions (e.g.
getCaseStudies()) - Add a corresponding schema in Studio
- Create a transformer and call it from the migration script
There’s even an example branch in the repo that shows how to handle a custom post type.
Common gotchas (and how to fix them)
Some issues you might hit:
1. “Cannot connect to WordPress API”
Check:
- Is
WP_API_URLcorrect? - Can you open
<WP_API_URL>/postsin your browser? - Is your WP site behind auth or a firewall?
2. “Sanity connection failed”
Verify:
-
SANITY_PROJECT_IDmatches your actual project - Token has read + write permissions
- Token hasn’t been revoked or expired
3. “Invalid image, could not read metadata”
This usually means there are corrupted or weird images in your WordPress media library. The script logs these but doesn’t fail the entire migration—fix them later if needed.
4. “JavaScript heap out of memory”
For very large sites, bump Node’s memory limit:
NODE_OPTIONS="--max-old-space-size=4096" npm run migrate
Plugging into your frontend
Once your content lives in Sanity, you can query it with GROQ and render with your framework of choice.
Example GROQ query:
*[_type == "post"] | order(publishedAt desc) {
title,
slug,
publishedAt,
excerpt,
body,
featuredImage,
categories[]->,
tags[]->
}
Example React/Next.js component with Portable Text:
import { PortableText } from "@portabletext/react";
export function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<PortableText value={post.body} />
</article>
);
}
From here, all the usual headless/modern-stack benefits kick in:
- Static generation or ISR
- Custom designs per post type
- Multi-channel publishing
- Better performance and SEO than a typical WP theme stack
Why we open-sourced it
We originally built this tool to migrate a real production blog to Sanity, without spending days writing one-off scripts and dealing with partial imports.
Instead of leaving it as an internal script, we decided to clean it up, document it properly, and release it under the MIT license.
If you:
- Are stuck on WordPress but want Sanity for content
- Don’t want to maintain fragile one-off migration scripts
- Prefer a starting point you can hack on
…this repo should save you a lot of time.
👉 Check it out, try it, and star it if it helps:
https://github.com/salttechno/wp-to-sanity-migration
Maintainers & support
This project is maintained by Salt Technologies, a software outsourcing company in India that builds modern web apps, headless CMS projects, and data/AI-powered platforms for clients across the globe.
🌐 Software Outsourcing Company in India – Salt Technologies
If you run into issues:
- Open a GitHub issue on the repo
- Share ideas for improvements (custom post types, progress UI, auth support, etc.)
- Or open a PR if you’ve already solved a problem others might hit
Happy migrating 🚀
Top comments (0)