Sanity keeps your content model in code. WordPress keeps it in a database.
Almost every real difference between the two comes out of that one sentence. Where the model lives decides how you review a change, how you copy it to staging, how a second developer picks it up, and what happens when someone edits the wrong thing on a Friday afternoon.
I have shipped sites on both. This is the comparison I give clients when they ask which one to put behind a Next.js front end, including the parts where WordPress is still the better answer.
Where the content model lives
Open a Sanity project and the shape of your content is sitting in files. A document type is a TypeScript object with a name, a list of fields and validation rules. It goes through pull requests like any other code, and a new developer who clones the repository has the entire model before they have logged into anything.
Open a WordPress project and the same information is scattered across database rows. Post types come from a function call or, on a lot of sites, from a plugin someone clicked through. Custom fields live in Advanced Custom Fields, which stores each field group as a post in the database with its configuration in post meta.
You cannot read that in a diff. You cannot revert it with a commit. To see what changed you compare two admin screens by eye.
| What you want to do | Sanity | WordPress with ACF |
|---|---|---|
| See what changed in the model | git diff | Compare two admin screens |
| Review a change before it ships | Pull request | Trust whoever made it |
| Undo a bad change | Revert the commit | Rebuild it by hand |
| Copy the model to staging | Deploy the branch | Export JSON, import JSON |
| Onboard a second developer | They clone the repo | They get an admin login |
| Know who changed a field | Commit history | Nothing, unless a plugin logs it |
Defining a content model
Take a small example. An article with a title, a slug, a publish date, a cover image with alt text and a rich text body.
In Sanity, the whole thing is one file.
import { defineType, defineField } from 'sanity'; export const article = defineType({ name: 'article', title: 'Article', type: 'document', fields: [ defineField({ name: 'title', type: 'string', validation: (rule) => rule.required().max(70), }), defineField({ name: 'slug', type: 'slug', options: { source: 'title', maxLength: 96 }, validation: (rule) => rule.required(), }), defineField({ name: 'publishedAt', type: 'datetime' }), defineField({ name: 'cover', type: 'image', options: { hotspot: true }, // Alt text is a field on the image, so an editor cannot upload // one without being asked for it. fields: [ defineField({ name: 'alt', type: 'string', validation: (rule) => rule.required(), }), ], }), defineField({ name: 'body', type: 'array', of: [{ type: 'block' }, { type: 'image' }], }), ],});That file is the model, the editing interface and the validation at once. Commit it, deploy the Studio, and every editor sees the change. The maximum title length is now enforced for everyone, and you can prove when it started being enforced.
The WordPress half of the same job splits in two. The post type goes in PHP.
<?phpadd_action('init', function () { register_post_type('article', [ 'label' => 'Articles', 'public' => true, 'show_in_rest' => true, // leave this out and the REST API returns nothing 'rest_base' => 'articles', 'supports' => ['title', 'editor', 'thumbnail', 'revisions'], ]); register_post_meta('article', 'published_at', [ 'type' => 'string', 'single' => true, 'show_in_rest' => true, // and again, once per field, or it stays invisible ]);}); // Everything else lives in the database. You build it by clicking through// Custom Fields, Field Groups, Add New, and repeating that per field.The rest happens in the browser. You add a field group, pick a field type from a dropdown, type a label, type a name, set required, set the return format, choose which post type it attaches to, and save. Then you do it again for the next field.
None of that is hard. It is just slow, and it is invisible to everyone who was not in the room.
The zeros are the point. Once a Sanity project exists, the model stops being something you administer and becomes something you write.
Seeding content over the API
This is where the gap gets wide, and it is the part clients underestimate. You will need to load content programmatically more often than you expect: migrating a few hundred old posts, building demo data so the design gets reviewed against real text, resetting a staging environment, or importing a product feed every night.
In Sanity you issue a write token in the project settings, drop it in an environment file, and write a script.
import { createClient } from '@sanity/client';import articles from './articles.json'; const client = createClient({ projectId: process.env.SANITY_PROJECT_ID!, dataset: process.env.SANITY_DATASET ?? 'production', apiVersion: '2026-01-01', // pinned by date, so an upgrade never surprises you token: process.env.SANITY_WRITE_TOKEN, useCdn: false,}); // Deterministic ids make the script safe to rerun. Run it twice and you// still have 40 documents, not 80.async function seed() { const tx = articles.reduce( (batch, item) => batch.createOrReplace({ _id: `article.${item.slug}`, _type: 'article', title: item.title, slug: { _type: 'slug', current: item.slug }, publishedAt: item.publishedAt, }), client.transaction(), ); const { results } = await tx.commit(); console.log('wrote', results.length, 'documents');} seed();One transaction, one commit, all of it or none of it. If the fortieth record is malformed, the first thirty nine do not land either, so you never have to work out how far the script got before it fell over.
Moving the whole dataset between environments does not need a script at all. The CLI does it, images included.
# Pull production down to a file, images and allnpx sanity dataset export production ./backup.tar.gz # Push it into staging, replacing what is therenpx sanity dataset import ./backup.tar.gz staging --replace # And the same command is your backup, so run it on a schedulenpx sanity dataset export production ./backups/$(date +%F).tar.gzWordPress can do this too. It takes more setup and more care.
import articles from './articles.json'; // Application passwords are generated per user in wp-admin, under Users,// Profile. Anyone who gets this string has that user's permissions.const auth = Buffer.from( `${process.env.WP_USER}:${process.env.WP_APP_PASSWORD}`,).toString('base64'); async function createArticle(item: Article) { const res = await fetch('https://cms.example.com/wp-json/wp/v2/articles', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${auth}`, }, body: JSON.stringify({ title: item.title, slug: item.slug, status: 'publish', meta: { published_at: item.publishedAt }, // needs show_in_rest on that meta key acf: { subtitle: item.subtitle }, // needs ACF REST support switched on }), }); if (!res.ok) throw new Error(res.status + ' ' + (await res.text())); return res.json();} // No transaction. If this loop dies at item 27, items 1 to 26 are live on// your site and you get to work out how to clean them up.for (const item of articles) { await createArticle(item);}Four things make this harder than it looks. Meta fields stay invisible to the API until you register each key with show_in_rest. ACF fields need their own REST support turned on before they appear. Images go up as a separate multipart request with a Content‑Disposition header, and then you attach the returned id to the post. And there is no transaction, so a failure halfway leaves you in a state you have to unpick by hand.
WP‑CLI is a better tool for this than the REST API, and if you have shell access on the box I would use it. That is the catch. Managed WordPress hosts often do not give you one, which is exactly when you fall back to the script above.
What editors actually get
Sanity Studio is a React application that you configure and deploy yourself. It gives your team live collaboration on the same document, real time previews, structured rich text, and custom input components when a field needs one. Because it is your code, you can hide fields, reorder them, group them into tabs and write a preview pane that renders the actual page.
It also has no plugin directory worth the name.
WordPress has twenty years of that. Gutenberg is familiar to a huge number of writers, the media library is good, user roles are mature, and if you need redirects or a form or an SEO checklist there is a plugin for it that someone on your team has already used.
Be honest about the retraining cost. If your marketing team publishes in WordPress every day and likes it, moving them to a new editor is a real project with a real morale cost, and it is not one you should take on just because the schema story is nicer for developers. That is exactly the case where keeping WordPress as the headless CMS is the right call: the editor never changes, and the public site still gets served as static files.
What each one costs
The sticker prices move, so check both pricing pages before you decide. The shape of the bill is stable, and the shape is what catches people out.
With headless WordPress you are still running a server. You pay for the origin, you pay an annual ACF Pro licence per site tier for the field types real projects need, and you usually pay for two or three more plugins on top. Then you pay in time, which never shows up on the invoice. I went through the hosting side in detail here.
With Sanity there is no server. The free tier covers small projects, and paid plans scale on seats, API usage and bandwidth. Your bill grows with traffic and team size rather than with the box you provisioned.
- Small brochure site, one editor. Sanity is usually free or close to it. WordPress costs you hosting plus the ACF Pro licence from the moment you need a repeater.
- Content site with a team. Sanity starts charging per seat. WordPress charges nothing extra for the tenth user, which is a genuine advantage for a large editorial group.
- High traffic. Both sit behind a static front end, so visitor traffic barely touches the CMS. Your bill is the front end, not the backend.
Note the second row. Per seat pricing is the one place where Sanity gets more expensive as you grow and WordPress does not, and if you have twenty writers it can decide the argument on its own.
What breaks later
A headless WordPress install is still a PHP application on the public internet. It still needs PHP version upgrades, plugin updates, core updates and someone paying attention to security advisories. Going headless removes the front end from the attack surface. It does not remove wp‑admin.
There is a supply risk too, and it is not theoretical. In October 2024 the WordPress.org plugin directory took over the Advanced Custom Fields listing and forked it as Secure Custom Fields, so sites updating from the directory started receiving a different plugin than the one their developers installed. Whatever you think of the dispute behind it, that is a dependency you do not control sitting underneath your content model.
Sanity moves that risk somewhere else rather than deleting it. You depend on a hosted service, and if it goes down your editors cannot publish, although a statically built site keeps serving. What you get back is that nobody has to patch it, and the API is pinned by date in your client config, so a change on their side cannot alter your responses until you bump that string yourself.
Then there is the everyday case, which is changing a field.
| Step | Sanity | WordPress with ACF |
|---|---|---|
| Make the change | Edit a file | Click through wp-admin |
| Test it | Run the Studio locally | Do it on staging, or on production |
| Review it | Pull request | None available |
| Ship it | Merge and deploy | Export JSON, import on production |
| Roll it back | Revert the commit | Reverse the clicks from memory |
When WordPress is the right pick
I recommend WordPress for a good share of the projects that come to me.
- Your editors are fluent and happy. A team that publishes daily in Gutenberg has real speed in that tool. Do not throw that away for developer comfort.
- You sell things. WooCommerce, memberships and subscriptions are years of work to replace and you will not enjoy any of it.
- A plugin is doing something load bearing. Events, bookings, a directory, a forum. If it works, rebuilding it is a project of its own.
- Multilingual is already running. If WPML or Polylang is set up and your translators know it, that is a lot of working machinery to hand back.
- Nobody is on call. If you have no developer after launch, a familiar system somebody local can pick up beats a nicer one nobody in your building knows.
In every one of those cases you can still take the slow front end away. Keep WordPress as the content source, build the public site in Next.js, and your editors never notice the difference except that the site got fast.
Decide it in one sitting
- Count your editors. More than about ten and per seat pricing starts to matter.
- List the plugins doing real work. Anything you cannot rebuild in a week argues for staying.
- Ask whether a developer will still be around in six months. If not, favour the familiar system.
- Ask how often the content model changes. Often means code, rarely means it matters less.
- Check whether you need commerce, memberships or multilingual. Any of those and WordPress usually wins.
- Look at who reviews changes today. If the answer is nobody, a schema in code fixes that for free.
If you want a hand with any of this, that is the work I do. I move WordPress sites onto Next.js without losing their rankings, I set up Sanity with the schema and a seed script so your content model lives in your repository from day one, and where it makes more sense I wire WordPress up as a headless CMS so your team keeps the editor it already knows. I work from Islamabad with clients across Europe, North America and the Gulf.
Tell me what you are running now and I will give you a straight answer on which of the two fits, including when the answer is to leave things alone. See how the migration work runs, or send me the details. You can also hire me on Fiverr if you would rather work through a platform with the reviews attached.
Questions people ask me
Do I need ACF Pro to run WordPress headless?
Not for a simple site. You need it the moment you want repeating sections, flexible page builders, galleries or cloned field groups, because those field types sit behind the paid licence. Most real marketing sites reach that point in the first week.
Can I move my content out of Sanity later?
Yes. The Sanity CLI exports a whole dataset to newline delimited JSON with the images alongside it, and you can run that export on a schedule as your backup. Rich text is stored as Portable Text, which is a documented open format you can convert to HTML or Markdown with a small script.
Is one of them better for SEO?
Neither. Your CMS does not decide your titles, canonicals or Core Web Vitals, your front end does. What matters is that whichever one you pick lets you store a title, a description, a canonical and a social image per page, and both do.
Can I use WordPress and Sanity together?
You can, and I would usually talk you out of it. Two content systems means two sets of credentials, two backup routines and a standing argument about which one owns a given page. Pick one and move everything into it.
How long does it take to switch a site from WordPress to Sanity?
The schema work is fast, often a day or two, because it is code. The slow part is migrating existing content, mapping old URLs to new ones and rebuilding whatever your plugins were doing. On a normal marketing site with a few hundred pages, plan for weeks rather than days.
Do you build these, and where are you based?
Yes. I am a full stack engineer in Islamabad and I do this work for clients worldwide, either moving a WordPress site onto Next.js with Sanity behind it, or keeping WordPress as the headless CMS if your team is happy in that editor.
If you have already decided to move and you want the mechanics, the migration guide covers redirects and metadata, and the checklist is the version you can hand to whoever does the work.
Resources
- Sanity: schema types
The full field type reference, which is the file you will spend most of your setup time in.
- Sanity: the JavaScript client
Transactions, createOrReplace and token auth, which is everything the seed script above uses.
- Sanity: dataset export and import
How to move a whole dataset between environments, and how to get your content back out.
- Portable Text
The open rich text format Sanity stores, so you can see what you would be converting from later.
- WordPress REST API handbook
Endpoints, authentication and the show_in_rest rules that decide whether your fields appear at all.
- WordPress application passwords
The built in way to authenticate a script against WordPress without installing a JWT plugin.
- ACF: local JSON
How to get ACF field groups into version control, if you stay on WordPress.
- WordPress.org on Secure Custom Fields
The October 2024 announcement behind the ACF fork, worth reading before you build on the plugin.