← Back to blog

Headless WordPress with Next.js: When It Makes Sense

· 10 min read · WordPress

A client comes to you with a brief: they want WordPress for content management but their marketing team keeps showing them Vercel demos with 100 Lighthouse scores and sub-second page loads. The CTO has read a blog post about headless architecture. Now everyone’s excited. The question isn’t “should we use WordPress?” — it’s “can we use WordPress without WordPress actually serving the pages?”

That’s the headless WordPress conversation, and it’s one I’ve had enough times to have a strong opinion on when it’s worth the complexity and when it’s just cargo-culting a pattern that doesn’t fit the problem.

What Headless WordPress Actually Means

The term gets thrown around loosely. Headless WordPress means WordPress handles content management — the admin interface, user roles, media library, editorial workflows — but does not render the frontend. Instead, it exposes content via an API (REST or GraphQL), and a separate frontend application — in this case, Next.js — fetches that content and renders the HTML.

WordPress becomes a backend CMS. Next.js becomes the presentation layer. The two talk over HTTP.

That’s the clean version. In practice, “headless WordPress” means you’re now maintaining two separate applications, two deployment pipelines, two sets of environment variables, and a synchronization layer between a CMS that was designed to render its own output and a frontend that has to reconstruct every behavior the CMS used to handle for free.

Preview mode, redirects, form handling, search, authentication — all of it needs a solution in the new architecture. Some of those solutions are elegant. Some are painful.

The API Layer: REST vs WPGraphQL

WordPress ships with a REST API out of the box. You can hit /wp-json/wp/v2/posts and get JSON back immediately, no plugins required. For simple use cases, this is perfectly fine.

The problem with the REST API at scale is over-fetching. A single post endpoint returns dozens of fields you don’t need — author details, meta, embedded data, link collections — and if you need data from multiple post types or custom fields, you’re making multiple requests or wrestling with the _embed parameter to bundle related data.

WPGraphQL solves this. It’s a plugin that exposes the entire WordPress data graph via a single GraphQL endpoint. You query for exactly what you need:

query GetPost($slug: String!) {
  postBy(slug: $slug) {
    title
    date
    content
    excerpt
    featuredImage {
      node {
        sourceUrl
        altText
      }
    }
    categories {
      nodes {
        name
        slug
      }
    }
    author {
      node {
        name
        avatar {
          url
        }
      }
    }
  }
}

One request, exactly the shape you need. No over-fetching, no multiple round-trips. WPGraphQL also integrates cleanly with ACF (Advanced Custom Fields) via the wpgraphql-acf plugin, which means your custom field data is queryable in the same request as the post content.

My default recommendation is WPGraphQL over the REST API for any serious headless implementation. The REST API is fine for quick prototypes or simple blogs, but the moment you have custom post types, complex relationships, or ACF fields, GraphQL becomes significantly less painful to work with.

Next.js Integration Patterns

Next.js and headless WordPress fit together well — better than most frontend frameworks — because Next.js was built with content-heavy sites in mind. The data fetching patterns map cleanly to how WordPress content works.

Static Generation with getStaticProps

For most content — blog posts, pages, portfolio items — static generation is what you want. Pages are built at deploy time, served from a CDN, and load instantly. Here’s a minimal implementation for a blog post page:

// pages/blog/[slug].js
import { gql } from '@apollo/client';
import client from '../../lib/apollo-client';

const GET_POST = gql`
  query GetPost($slug: String!) {
    postBy(slug: $slug) {
      title
      content
      date
      excerpt
      featuredImage {
        node {
          sourceUrl
          altText
        }
      }
    }
  }
`;

export async function getStaticProps({ params }) {
  const { data } = await client.query({
    query: GET_POST,
    variables: { slug: params.slug },
  });

  if (!data.postBy) {
    return { notFound: true };
  }

  return {
    props: {
      post: data.postBy,
    },
    revalidate: 60, // ISR: rebuild this page every 60 seconds if requested
  };
}

export async function getStaticPaths() {
  // Fetch all post slugs to pre-render at build time
  const { data } = await client.query({
    query: gql`
      query AllPostSlugs {
        posts(first: 100) {
          nodes {
            slug
          }
        }
      }
    `,
  });

  return {
    paths: data.posts.nodes.map(({ slug }) => ({
      params: { slug },
    })),
    fallback: 'blocking', // new posts are server-rendered on first request
  };
}

The revalidate key is what enables Incremental Static Regeneration (ISR). Rather than rebuilding the entire site every time a post is updated, Next.js will serve the cached static version and trigger a background rebuild when the page is requested after the revalidation window expires. For a content-heavy site with hundreds of posts, this is significantly better than full rebuilds.

On-Demand Revalidation

ISR with a time-based window means content changes aren’t reflected immediately. For most blogs, a 60-second delay is acceptable. For news sites or frequently updated content, you want on-demand revalidation triggered by WordPress itself.

The pattern: set up a WordPress hook that fires a request to your Next.js revalidation API route whenever a post is published or updated.

// WordPress functions.php — trigger revalidation on save
add_action('save_post', function($post_id) {
    if (wp_is_post_revision($post_id) || get_post_status($post_id) !== 'publish') {
        return;
    }

    $slug = get_post_field('post_name', $post_id);
    $revalidate_url = 'https://your-nextjs-site.com/api/revalidate';

    wp_remote_post($revalidate_url, [
        'body' => json_encode([
            'secret' => defined('REVALIDATE_SECRET') ? REVALIDATE_SECRET : '',
            'slug'   => $slug,
        ]),
        'headers' => ['Content-Type' => 'application/json'],
        'blocking' => false, // fire and forget
    ]);
});
// pages/api/revalidate.js — Next.js revalidation endpoint
export default async function handler(req, res) {
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  const { slug } = req.body;

  try {
    await res.revalidate(`/blog/${slug}`);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

This works well. The friction is the setup and the operational overhead: you’re now maintaining a webhook-like system between two applications, and when it breaks — wrong environment variable, network timeout, deployment mismatch — posts stop appearing on the live site until someone investigates.

The Preview Workflow Problem

This is where headless WordPress gets frustrating, and most tutorials gloss over it.

In traditional WordPress, previewing a draft post is a single click. WordPress renders the draft in the same template as the live post. The editor sees exactly what visitors will see. It’s seamless because both the editing and the rendering happen in the same application.

In a headless setup, your Next.js frontend only knows about published content. A draft post in WordPress doesn’t exist in your static build. To preview it, you need to:

  1. Enable Next.js Preview Mode — a special mode that bypasses static generation and hits the API directly
  2. Set up a WordPress “preview” button that redirects to your Next.js site with a signed token
  3. Build an API route in Next.js that validates the token and enables preview mode
  4. Modify every getStaticProps function to detect preview mode and fetch draft content instead of published content
  5. Configure WPGraphQL authentication so your Next.js app can fetch unpublished posts

That’s five moving parts for what is a one-click action in traditional WordPress. The implementation isn’t impossible — WPGraphQL has a preview plugin and there are documented patterns for this — but it’s a meaningful development investment, and it’s easy to get wrong. Authentication tokens expire. Preview cookies behave unexpectedly. Editors complain that preview looks different from the live site because preview bypasses ISR caching.

I’m not saying don’t do it. I’m saying go in knowing this will take real time to implement properly, and your editors will surface edge cases you didn’t anticipate.

What You Lose Going Headless

The WordPress plugin ecosystem is one of its core value propositions. A massive portion of that ecosystem assumes WordPress is rendering the frontend. Going headless doesn’t just mean losing some plugins — it means losing the entire category of plugins that work by hooking into WordPress’s output.

Page builders like Elementor, Divi, or Beaver Builder? Gone. They exist to generate frontend markup, which is now irrelevant. Contact Form 7 or Gravity Forms? You can still use them for form processing, but you’ll need custom frontend implementations to display and submit them. WooCommerce in a headless setup is a significant project on its own — cart state, checkout, authentication, Stripe integration all need custom solutions in your Next.js app.

SEO plugins like Yoast or RankMath still work for managing meta data, and WPGraphQL exposes their data, but you’re responsible for rendering that meta data correctly in Next.js. It’s not automatic.

Performance-related plugins — caching plugins, image optimization plugins — either become irrelevant or need to be reconsidered. Next.js handles image optimization and caching natively, so you’re not losing the capability, you’re just replacing one set of tools with another.

Should You Use WordPress or Switch to Sanity/Contentful?

This question comes up regularly when a team decides they want a headless CMS. WordPress wasn’t designed to be a headless CMS. Sanity and Contentful were. That difference shows up in specific places.

Sanity has a real-time preview API that’s dramatically better than WordPress’s preview mode implementation. Contentful has structured content modeling that’s cleaner than ACF custom fields. Both have developer experiences that are purpose-built for the decoupled architecture.

So why keep WordPress? A few legitimate reasons:

  • Existing content. Migrating years of content from WordPress to a new CMS is a real project. The cost often outweighs the DX benefits of a purpose-built headless CMS.
  • Editorial familiarity. Your editors know WordPress. Retraining a content team on Sanity’s block editor or Contentful’s interface has a cost that doesn’t show up in technical comparisons.
  • Plugin-dependent workflows. If your operation depends on WooCommerce, Events Manager, or a complex ACF setup, rebuilding that in Contentful is a separate project from just switching CMSes.
  • Cost. WordPress is free. Contentful and Sanity have usage-based pricing that can get expensive at scale.

If you’re starting a new project with no existing content and no WordPress-specific requirements, a purpose-built headless CMS is a reasonable choice. If you have an existing WordPress site with years of content and established editorial workflows, the pragmatic answer is usually to keep WordPress and invest in a clean headless implementation rather than migrating platforms.

When Headless WordPress Actually Makes Sense

Headless WordPress is not always the right choice. It’s the right choice in specific situations:

You need a frontend that WordPress can’t deliver. Complex interactive applications, highly custom design systems that don’t fit a WordPress theme model, React-based UIs with state management that would be awkward in a WordPress context — these are legitimate reasons. If your frontend requirements have outgrown what WordPress themes can express cleanly, headless is a reasonable architectural response.

You’re building for multiple channels. If the same content needs to feed a website, a mobile app, and a third-party integration, an API-first architecture makes the content reusable. Traditional WordPress doesn’t give you this cleanly.

Performance is a hard requirement, not an aspiration. A well-optimized traditional WordPress site with good hosting, proper caching, and image optimization can perform very well. But a Next.js frontend serving statically generated pages from a CDN has a structural performance advantage for page load times that’s difficult to match with server-rendered WordPress. If your performance requirements are strict and your content fits a static generation model, headless has a real advantage.

Your team has frontend expertise. Headless WordPress in the hands of a strong React/Next.js team moves fast. Headless WordPress handed to a team that primarily knows PHP and WordPress theme development moves slowly and produces fragile implementations. The architecture choice needs to match the team.

When Headless Is Overkill

Most WordPress sites don’t need to be headless. That’s not a hedged opinion — it’s a direct one.

If you’re building a business website, a portfolio, a blog, a small e-commerce store, or a marketing site for a company, traditional WordPress with a well-built theme, good hosting, and proper caching will serve you well. The performance gap between a properly configured WordPress site and a headless Next.js frontend is real but often not meaningful for the actual users of a business website.

The engineering complexity of headless — two applications, two deployments, preview workflow, plugin limitations, the operational surface area of keeping a WPGraphQL endpoint and a Next.js frontend in sync — is a real ongoing cost. That cost needs to be justified by genuine requirements, not by architecture enthusiasm.

A client who wants WordPress because their team knows it, and wants a fast website, is not automatically a candidate for a headless setup. They might be better served by a traditional WordPress site with a lightweight theme, Cloudflare in front of it, and a good image optimization setup. That’s not a lesser solution. That’s the right solution for the problem.

The headless conversation tends to happen when someone has read about the architecture and finds it compelling, not because there’s a specific problem that only headless solves. Those are different starting points, and they lead to different decisions.

Making the Right Call

The question isn’t “is headless better than traditional WordPress?” The question is “what does this specific project actually need, and what’s the real cost of each approach for this team?”

I’ve built both. Traditional WordPress sites with custom themes that perform well and are easy for clients to maintain. Headless WordPress setups with Next.js frontends where the architecture earned its complexity. The pattern recognition comes from having built enough of both to know which problems each architecture solves and which problems it creates.

If you’re a CTO or technical lead evaluating this decision, the honest framing is: headless WordPress with Next.js is a significant technical investment that pays off under specific conditions. Knowing those conditions is the prerequisite to making the decision well.

If you’re working through this evaluation for a real project — weighing whether to go headless, which API approach to use, how to handle the operational complexity, or whether WordPress is even the right CMS — I’m available to work through it. That kind of technical advisory conversation is exactly where I spend a lot of time with clients before any code is written.