Skip to main content
ASoc
Comparison

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.

The ASoc Team9 min read

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

StrapiHeadless WordPress
What it isNode/TypeScript CMS, API-first by designWordPress with the theme layer bypassed
RuntimeNode process plus Postgres/MySQL/SQLitePHP-FPM plus MySQL
APIREST and GraphQL, generated from your typesREST out of the box; GraphQL via WPGraphQL
Content modelContent-Type Builder writes JSON schema filesCustom post types and ACF field groups, in the database or in code
Schema in version controlYes — src/api/**/schema.jsonOnly if you register types in a plugin; ACF's UI state is DB rows
Editor familiarityNew UI to learnThe one your editors already know
Plugin ecosystemSmall, Node-nativeEnormous, and written for the rendered theme you just removed
Media handlingUpload provider you configureBuilt-in library, plus whatever CDN plugin you add
Auth for the APIAPI tokens, role-basedApplication passwords, JWT plugin, or a proxy
Who patches it at 2amYouYou, and there is more of it
Hosting floorOne Node app, one databasePHP 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 --prod the current main until the Supabase env vars are set in the Vercel project: src/proxy.ts runs on every request and will error site-wide without NEXT_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/users to 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

SymptomCauseFix
Whole site 500s after a deployA content fetch runs per request and an env var is missingFetch at build, revalidate on a webhook; fail a missing var at build time, not at request time
WordPress SEO tags vanished going headlessYoast/RankMath filter the theme's <head>, which no longer runsRead their meta from the API (Yoast exposes it), or generate metadata in the front end
One post needs five API callsREST returns IDs, not relationsWPGraphQL, or Strapi's populate, and ask for the whole page in one query
Content updates never appearPages were prerendered and nothing told them to rebuildWebhook → revalidation route; never a timer
Strapi upgrade breaks the buildMajors have migration steps, and the generated types movedPin the version, upgrade deliberately, regenerate types in CI
/wp-json/wp/v2/users lists your staffThe REST API is public by defaultRestrict unauthenticated endpoints; do not rely on obscurity
Editors "lost" the previewHeadless removed the theme the preview renderedPoint 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.

Keep reading

Comparison8 min read

Strapi vs Payload CMS: Where the Content Model Lives

Strapi's GUI writes your schema; Payload's schema is TypeScript you review. That one difference decides the rest — measured against 391 tests and no CMS at all.

Read more
Comparison9 min read

Supabase vs. AWS: What Five Services Replace One Dependency

Supabase bundles Postgres, auth, storage and atomic RPCs behind one SDK. Mapped against this app's real files, here's what RDS, Cognito, S3 and Lambda would cost to assemble instead.

Read more