← Back to all posts
August 29, 2026
•

How to Build a Headless WordPress Site Using React (Step by Step)

React logo on a computer screen, illustrating headless WordPress development using React

Most requests we get for headless WordPress development using React start the same way. The marketing team likes the WordPress editor and does not want to give it up. The engineering team wants a React front end they control, with a real build pipeline and a CDN in front of it. Nobody wants to maintain two content systems. Headless gives you both: WordPress stays the editorial back end, React (almost always through Next.js) renders the front end, and the two talk over the REST API or WPGraphQL.

This guide walks through the build we run for clients on our headless WordPress development service: hosting layout, API choice, the Next.js data layer, previews, cache revalidation and deployment. It is written for a CTO or technical founder deciding whether to build this in-house or hire it out. The short version: headless is worth it when the front end carries real product logic or serious traffic. It is not worth it for a five-page brochure site.

when headless WordPress development using React makes sense

Before writing code, be honest about the trade. A traditional WordPress theme gives you thousands of plugins that render on the front end for free: forms, sliders, membership gates, WooCommerce. Go headless and every one of those becomes an integration you own. The cases where that trade pays off are consistent across the projects we have shipped:

  • Content reused across surfaces. The same posts feed a website, a mobile app and an in-product help centre. One CMS, several front ends.
  • Front end with product logic. Dashboards, configurators, calculators or anything where React state management matters more than a page builder.
  • Traffic spikes. Statically generated pages on a CDN survive a launch-day spike that would knock over a PHP-rendered site on the same budget.
  • Engineering team already on React. If your developers live in TypeScript, forcing them into PHP templates costs you velocity.

If none of those apply, a well-built Bricks or block theme with good caching will be cheaper to build and far cheaper to maintain. We say this to prospects regularly and it is the reason our full website development quotes sometimes come back as “do not go headless.”

step 1: separate the CMS from the front end

Run WordPress on its own hostname, typically cms.example.com, and the React app on the public domain. WordPress can sit on Lightsail, EC2 or a managed host; the front end goes to Vercel, Netlify, AWS Amplify or CloudFront in front of S3. If the WordPress side is moving to AWS as part of the project, our AWS hosting migration for WordPress work usually happens in the same sprint so the API origin is stable before the front end is built against it.

The WordPress front end still exists after this split, and search engines will find it if you let them. Lock it down with a small must-use plugin that redirects any non-admin, non-API, non-preview request to the React domain:

<?php
// wp-content/mu-plugins/headless-redirect.php
add_action( 'template_redirect', function () {
    if ( is_admin() || wp_doing_ajax() || is_preview() ) {
        return;
    }
    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
        return;
    }
    $path = $_SERVER['REQUEST_URI'] ?? '/';
    wp_redirect( 'https://www.example.com' . $path, 301 );
    exit;
} );

Set WP_HOME to the public domain and WP_SITEURL to the CMS domain in wp-config.php. That single change makes permalinks, sitemaps and canonical tags point at the React site while the admin keeps working.

step 2: choose the API: REST or WPGraphQL

Both work. The choice depends on how much nested data your pages need and whether your team already thinks in GraphQL.

Concern WP REST API WPGraphQL
Setup Built into core Plugin plus WPGraphQL for ACF
Over-fetching Use _fields and _embed to trim Ask for exactly the fields you need
Nested data (post, author, terms, ACF) Several requests or custom endpoints One query
Caching Cache-friendly GET URLs Needs WPGraphQL Smart Cache or persisted queries
Auth for previews Application Passwords WPGraphQL JWT Authentication

For sites with heavy ACF usage we lean toward WPGraphQL. For simpler content models we stay on REST and add a few custom endpoints, which is exactly the pattern covered in our guide to custom REST API integration for WordPress. Either way, custom post types need show_in_rest (or show_in_graphql) set to true or they will not appear at all.

ACF fields do not reach the REST API unless you enable “Show in REST API” on each field group, and even then they arrive in a flat acf object. When we need computed values, for example a resolved image with srcset data, we register a field explicitly:

<?php
add_action( 'rest_api_init', function () {
    register_rest_field( 'post', 'hero', [
        'get_callback' => function ( $post ) {
            $image_id = get_field( 'hero_image', $post['id'] );
            return [
                'heading' => get_field( 'hero_heading', $post['id'] ),
                'image'   => $image_id ? wp_get_attachment_image_src( $image_id, 'large' ) : null,
                'srcset'  => $image_id ? wp_get_attachment_image_srcset( $image_id, 'large' ) : null,
            ];
        },
        'schema' => [ 'type' => 'object' ],
    ] );
} );

step 3: build the Next.js data layer

Keep every WordPress call behind one module. When the API changes, and it will, you edit one file. Use the App Router with fetch tags so pages can be revalidated individually later:

// lib/wp.ts
const API = process.env.WP_API_URL; // https://cms.example.com/wp-json/wp/v2

export async function getPost(slug: string) {
  const res = await fetch(
    `${API}/posts?slug=${slug}&_embed=1&_fields=id,slug,title,content,excerpt,hero,_links,_embedded`,
    { next: { tags: ['post', `post:${slug}`] } }
  );
  if (!res.ok) throw new Error(`WP ${res.status}`);
  const [post] = await res.json();
  return post ?? null;
}

export async function getAllSlugs() {
  const res = await fetch(`${API}/posts?per_page=100&_fields=slug`, {
    next: { tags: ['post'] },
  });
  return (await res.json()).map((p: { slug: string }) => p.slug);
}

In app/blog/[slug]/page.tsx, generateStaticParams calls getAllSlugs() so every published post is built ahead of time. The rendered content.rendered string goes through html-react-parser with a replace map that swaps WordPress image tags for next/image and internal links for next/link. If you need block-level control, WPGraphQL Content Blocks exposes each Gutenberg block as structured JSON and you render React components per block name. That is more work up front but it is how you get a truly custom front end rather than a restyled HTML dump.

menus, media and head tags

Three things catch teams out here. The core menu endpoints under /wp/v2/menus require authentication, so either add a public read-only endpoint or use WPGraphQL, which exposes menus openly. Media URLs still point at the CMS domain, so add it to images.remotePatterns in next.config.js. And SEO metadata: Rank Math exposes a rankmath/v1/getHead endpoint that returns the full head markup for a URL, which you can parse into Next.js generateMetadata. Yoast does the same through its yoast_head_json field. Do not hand-build titles and descriptions in React when the SEO plugin already computes them.

step 4: previews that editors will actually use

A headless site where “Preview” opens a broken CMS page will get rejected by the content team in the first week. Point the preview link at the front end:

<?php
add_filter( 'preview_post_link', function ( $link, $post ) {
    $secret = defined( 'HEADLESS_PREVIEW_SECRET' ) ? HEADLESS_PREVIEW_SECRET : '';
    return add_query_arg( [
        'secret' => $secret,
        'id'     => $post->ID,
        'type'   => $post->post_type,
    ], 'https://www.example.com/api/preview' );
}, 10, 2 );

The Next.js route handler checks the secret, enables draftMode(), fetches the draft with an Application Password over Basic auth (draft posts are only returned to authenticated requests), and redirects to the page. Create a dedicated WordPress user with the Editor role for that Application Password and store it in the front end host’s secret manager, never in the repo.

step 5: cache invalidation on publish

Static generation is only useful if publishing a post updates the live site within seconds. Hook the status transition in WordPress and call the front end’s revalidation endpoint:

<?php
add_action( 'transition_post_status', function ( $new, $old, $post ) {
    if ( 'publish' !== $new && 'publish' !== $old ) {
        return;
    }
    wp_remote_post( 'https://www.example.com/api/revalidate', [
        'timeout' => 5,
        'headers' => [ 'Content-Type' => 'application/json' ],
        'body'    => wp_json_encode( [
            'secret' => HEADLESS_REVALIDATE_SECRET,
            'tags'   => [ 'post', 'post:' . $post->post_name ],
        ] ),
    ] );
}, 10, 3 );

The route handler calls revalidateTag() for each tag. Because the fetch calls in step 3 were tagged, only the affected post and the listing pages rebuild. Full site rebuilds on every save are the number one reason headless projects feel slow to editors; tag-based revalidation removes that problem entirely.

step 6: harden and monitor the API

The CMS is now an API origin, so treat it like one. Put Redis object caching in front of the database (wp redis enable after installing the Redis Object Cache plugin), and cache REST GET responses at the CDN or with a plugin such as WP REST Cache. Restrict CORS to your front end domain by filtering rest_pre_serve_request rather than allowing every origin. Block user enumeration on /wp/v2/users for unauthenticated requests through rest_authentication_errors. After deployment, run wp rewrite flush and wp cache flush so stale permalinks do not leak into the API responses.

On the front end, keep an eye on the metrics that matter. Headless does not automatically make a site fast; a Next.js app that ships 800 KB of JavaScript will score worse than a lean PHP theme. The fixes are covered in our post on Core Web Vitals optimization for WordPress, and most of them apply to the React side as well.

what usually goes wrong

  • Forms. Gravity Forms and WPForms render on the WordPress front end. You will either rebuild forms in React and post to their REST endpoints, or move to a form service.
  • Search. The core /wp/v2/search endpoint works but is basic. Sites with large archives usually move to Algolia or Typesense with an indexer plugin.
  • Redirects. The Redirection plugin’s rules only fire on the WordPress front end. Export them into the Next.js redirects() config or a middleware.
  • Comments and logged-in content. Both are possible over the API but require real engineering time. Scope them explicitly.
  • Plugin updates. A plugin update that changes REST output can break the front end silently. Pin versions and test the API contract in CI.

when to do this yourself vs hire someone

If you have a developer comfortable with both Next.js and WordPress hooks, and the content model is simple, you can follow the steps above and have a working site in two to three weeks. Where it makes sense to hire is when the content model is deep (nested ACF, several post types, multilingual), when editorial workflow matters (scheduled posts, preview for every post type, revision comparison), or when the site cannot afford downtime during cut-over. Those are the projects where the second half of the build, the boring integration work, takes longer than the first half, and where experience with the failure modes above saves real money. Headless WordPress development using React is not hard to start; it is hard to finish well.

talk to us about your headless build

Our headless WordPress development using React engagements run on a fixed scope and fixed price agreed in writing before work starts, and you work directly with the engineers writing the code. Send us a short description of your content model, your hosting and the front end features you need through our contact page, and you will have a straight answer on scope, timeline and cost within 24 hours.

Share this post

keep reading

Portable external hard drive connected to a laptop, illustrating an automated WordPress backup system
September 22, 2026
•

Automated WordPress Backup System Setup: Offsite, Versioned and Actually Tested

Automated WordPress backup system setup done properly: offsite S3 storage, versioned retention, write-only credentials, hourly store backups, tested restores.

Read more
Page speed test results on a monitor, illustrating Core Web Vitals optimization for WordPress
September 20, 2026
•

Core Web Vitals Optimization for WordPress: LCP, INP and CLS Fixes That Actually Work

Core Web Vitals optimization for WordPress, metric by metric: the LCP, INP and CLS fixes in theme, plugins and server that move…

Read more
Glass cloud icon with data layers above a padlock, illustrating AWS hosting migration for WordPress
September 18, 2026
•

AWS Hosting Migration for WordPress: Lightsail vs EC2 vs Managed Hosting

AWS hosting migration for WordPress compared: Lightsail vs EC2 with RDS vs managed hosting, the migration steps we run, and what breaks…

Read more

want this kind of thinking on your project?

Tell us what you are building and we'll come back with a straight answer on scope, timeline and cost within 24 hours.
start a project