Strapi vs Headless WordPress: Two Servers, One Unasked Question
Both put a server and a database between your pages and your words. Measured against a build that serves 702 static pages in 38s with no content server at all.
Strapi is an API-first Node CMS you install and operate. Headless WordPress is a full WordPress install with the theme layer switched off, serving JSON. Both put a server and a database between your pages and your words. Choose Strapi for a clean content model, headless WordPress when editors already live in WordPress and migrating them costs more than running PHP.
The comparisons you will find elsewhere argue about REST versus GraphQL, plugin counts and admin ergonomics. Those are real, but they are downstream of one question that decides the outcome and almost never gets asked: does this content need a server at request time at all?
The comparison that matters
| Strapi | Headless WordPress | |
|---|---|---|
| What it is | Node/TypeScript CMS, API-first by design | WordPress with the theme layer bypassed |
| Runtime | Node process plus Postgres/MySQL/SQLite | PHP-FPM plus MySQL |
| API | REST and GraphQL, generated from your types | REST out of the box; GraphQL via WPGraphQL |
| Content model | Content-Type Builder writes JSON schema files | Custom post types and ACF field groups, in the database or in code |
| Schema in version control | Yes — src/api/**/schema.json | Only if you register types in a plugin; ACF's UI state is DB rows |
| Editor familiarity | New UI to learn | The one your editors already know |
| Plugin ecosystem | Small, Node-native | Enormous, and written for the rendered theme you just removed |
| Media handling | Upload provider you configure | Built-in library, plus whatever CDN plugin you add |
| Auth for the API | API tokens, role-based | Application passwords, JWT plugin, or a proxy |
| Who patches it at 2am | You | You, and there is more of it |
| Hosting floor | One Node app, one database | PHP host, database, object cache, and a WAF you will want |
The two rows that decide it are editor familiarity and who patches it at 2am. Everything above them is a preference you can live with either way.
The plugin row deserves a warning rather than a tick. WordPress's ecosystem is its strongest argument in a normal install and its weakest in a headless one, because most of those plugins work by filtering what the theme renders. Go headless and Yoast stops writing your <head>, page builders stop producing pages, and caching plugins cache a JSON response nobody asked them to. You keep the admin and lose the half of the ecosystem that made the admin worth it.
What both of them actually cost
Here is the part the vendor comparisons leave out, measured on the site you are reading.
This storefront publishes 277 blog posts. They are MDX files in the repository, compiled at build time. The whole production build looks like this:
✓ Compiled successfully in 17.3s
Finished TypeScript in 7.7s
✓ Generating static pages using 3 workers (702/702) in 10.9s
real 0m38.263s
702 static pages — 277 posts, 277 generated OG images, 111 product pages and the rest of the site — in 38 seconds, with no content server involved. At request time there is no CMS to reach, no database to connect to and no API token to rotate. A post cannot fail to render because something else was rebooting.
That is the baseline either CMS is measured against, and it is not an argument that you should never run one. It is an argument for knowing the number.
The runtime dependency is the real purchase
Add Strapi or headless WordPress and your pages acquire a dependency they did not have. It is easy to underrate until it bites, so here is the local evidence.
This codebase already took exactly one runtime dependency, for auth rather than content: src/proxy.ts refreshes the Supabase session on every request. The project's own CLAUDE.md carries a standing warning about it:
Do NOT
vercel --prodthe currentmainuntil the Supabase env vars are set in the Vercel project:src/proxy.tsruns on every request and will error site-wide withoutNEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_ANON_KEY.
One missing environment variable, and every page 500s — including the 277 posts that need nothing from Supabase at all. That is what "the CMS is a runtime dependency" means in practice, and it applies identically to a Strapi instance and to a WordPress API host. The blast radius of a content backend is not the content; it is every route that touches it.
Both CMSs have the same mitigation, and it is the one worth designing for up front: fetch at build time, render static, and revalidate on a webhook. Do that and the CMS is a build-time dependency, which is a much smaller thing to own.
// Static at build, refreshed when the CMS says so — not on a timer.
export const revalidate = false;
export async function generateStaticParams() {
const res = await fetch(`${process.env.CMS_URL}/api/posts?fields[0]=slug`, {
headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
});
const { data } = await res.json();
return data.map((p: { slug: string }) => ({ slug: p.slug }));
}
Pair it with a revalidation route the CMS calls on publish. Strapi does this with a webhook in Settings; WordPress does it with a save_post hook. A timer-based revalidate: 60 looks equivalent and is not — it re-fetches whether or not anything changed, and it still leaves a window where the site is stale.
Pick headless WordPress when the editors decide
The honest case for headless WordPress has nothing to do with technology. It is that you have editors — maybe dozens — who know WordPress, whose workflow is built around it, and whose training cost is a real line item. Keeping their admin and changing only the front end is a smaller change than it looks, and it is frequently the right one.
Make it with clear eyes:
- Register custom post types and fields in code, in a small must-use plugin, not through the ACF UI. Otherwise your content model lives in database rows and cannot be code-reviewed, diffed or promoted between environments.
- Install WPGraphQL if the front end will ever need more than one entity per page. The REST API forces a request per relationship, and a post with an author, a category and three related items is five round trips.
- Lock the REST API down. A default install answers
/wp-json/wp/v2/usersto anyone who asks. - Budget for the WordPress security surface anyway. Going headless removes the theme, not the attack surface of the admin.
Pick Strapi when the content model is the point
Strapi's argument is that the model is clean and the generated API matches it. The Content-Type Builder writes JSON schema files into src/api/**, so the model is in version control, and the TypeScript types come from the same source. If your content is genuinely structured — products with variants, events with venues, a documentation tree — that fidelity is worth a lot more than a plugin directory.
Its weaknesses are equally concrete. The ecosystem is small enough that you will write plugins rather than install them. Major upgrades have historically been real migrations. And you are now running a Node service whose uptime is your pages' uptime, which returns you to the section above.
What we do instead, and when it stops working
Neither, for this site — and that is a position with an expiry date, not a recommendation.
Content here is three files that must agree, enforced by a test rather than by a schema:
// src/data/blog.ts — the typed registry
export interface BlogPostMeta {
slug: string;
title: string;
description: string;
date: string;
cluster: BlogCluster;
targetKeyword: string;
readingMinutes: number;
relatedTemplates: string[];
relatedSpokes: string[];
tags: string[];
}
The prose lives at src/content/blog/<slug>.mdx, and src/lib/blog.ts maps each slug to an explicit dynamic import. npm test runs 391 assertions in 3.3 seconds across 31 files, and eleven of them exist only to keep those three files in sync — a post with no loader fails, a loader with no post fails, a relatedTemplates entry naming a product that no longer exists fails.
This works because everyone who writes here can open a pull request. The first editor who cannot is the day it stops working, and the choice above becomes live. Recognising that day early matters more than picking correctly today, because migrating out of files is straightforward and migrating between two CMSs is not. Sanity vs headless WordPress asks the same question with a hosted backend on the other side, and MDX vs a headless CMS is the long form of where the line falls.
Mistakes and how they show up
| Symptom | Cause | Fix |
|---|---|---|
| Whole site 500s after a deploy | A content fetch runs per request and an env var is missing | Fetch at build, revalidate on a webhook; fail a missing var at build time, not at request time |
| WordPress SEO tags vanished going headless | Yoast/RankMath filter the theme's <head>, which no longer runs | Read their meta from the API (Yoast exposes it), or generate metadata in the front end |
| One post needs five API calls | REST returns IDs, not relations | WPGraphQL, or Strapi's populate, and ask for the whole page in one query |
| Content updates never appear | Pages were prerendered and nothing told them to rebuild | Webhook → revalidation route; never a timer |
| Strapi upgrade breaks the build | Majors have migration steps, and the generated types moved | Pin the version, upgrade deliberately, regenerate types in CI |
/wp-json/wp/v2/users lists your staff | The REST API is public by default | Restrict unauthenticated endpoints; do not rely on obscurity |
| Editors "lost" the preview | Headless removed the theme the preview rendered | Point preview at a draft route on the front end, with a signed token |
Frequently asked questions
Is Strapi faster than headless WordPress? At the API level, usually — it is a Node service designed for JSON, where WordPress assembles a response through a filter chain built for HTML. But if you render statically, both run at build time and the difference stops being visible to visitors. Measure the build, not the endpoint.
Can I keep WordPress for editors and Strapi for structured data? You can, and some teams do. You are then operating two CMSs, two databases and a sync story between them. It is defensible when a large editorial team and a structured catalog genuinely have nothing to do with each other, and it is expensive when they do.
Which is cheaper? The licences are both zero. The cost is the operation: one Node process and a database for Strapi; PHP, MySQL, an object cache and security maintenance for WordPress. WordPress's floor is lower because commodity hosting exists for it; its ceiling is higher because the surface you maintain is larger.
Do I need a CMS for a marketing site at all? Only when someone who cannot open a pull request needs to publish without a deploy. Until then, typed content files give you review, history and a build that cannot fail from someone else's outage. It is worth writing that trigger down, because teams usually adopt a CMS a year after they needed one or two years before.
Templates in this post
ASoc Coin — an online-banking marketing site — ASoc Compound, an automated-investing site with goal-based portfolios, and ASoc Cortex, an AI-agency site with case studies, all ship typed Next.js and Tailwind source, so the listing and article components are ready for whichever content source you attach — or for none.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
