Uncategorized

Headless WordPress with Next.js Tutorial

Headless WordPress with Next.js Tutorial

I’ve lost count of how many headless WordPress builds I’ve shipped at this point. Some were greenfield projects where I got to make every decision. Others were rescue jobs — a client had a beautiful Next.js front-end bolted onto a WordPress back-end that fell over the moment real traffic showed up, and I got the call to figure out why.

So this isn’t a “copy these ten commands and you’re a headless developer” post. There are plenty of those, and most of them stop right at the part where things get interesting. This is closer to how I’d talk you through it if you hired me and asked, “okay, so how does this actually work, and what’s going to bite us?”

Let’s get into it.

What “headless” actually means here

The short version: you take WordPress and tell it to stop worrying about how the site looks. No themes, no PHP templates rendering your pages. WordPress becomes a pure content engine — editors still log in to the dashboard they already know, write posts, manage products, upload images — but the actual public-facing site is built and served by Next.js.

The two halves talk over an API. WordPress exposes your content (REST or GraphQL), Next.js fetches it, and React renders it however you want.

People hear “decoupled” and assume it’s more complicated for editors. It’s the opposite. The people writing content never see the change. Their wp-admin looks identical. The complexity moves to the developer’s side — which is exactly where you want it, because that’s the side that’s getting paid to handle it.

Why I reach for this setup (and when I don’t)

I’m not religious about headless. A small brochure site for a local business? I’ll build that in WordPress with a good page builder and be done in a fraction of the time. Headless earns its keep when one of these is true:

Performance is a real business problem. Next.js gives you static generation and server-side rendering, so pages can ship as pre-built HTML. On an e-commerce build I did, moving the storefront to a Next.js front-end took the largest contentful paint from “the client is embarrassed” to “sub-second.” That’s not a vanity metric — slow stores lose sales, and Google rankings track Core Web Vitals.

The front-end needs to do things WordPress fights you on. Heavy interactivity, app-like behavior, animations, real-time stuff. The moment you’re wrestling a theme to do something React does in ten lines, you’re on the wrong tool.

You want WordPress as one source among several. Headless makes WordPress just an API. You can pull content into a mobile app, a digital signage screen, three different sites — all from the same dashboard.

Security matters and you want a smaller attack surface. The public site has no PHP, no exposed wp-login on the domain people actually visit. WordPress sits behind it. Most of the automated junk that hammers WordPress sites never finds a door.

If none of those apply, classic WordPress is probably the faster, cheaper, more maintainable choice — and I’ll tell a client that rather than sell them complexity they don’t need.

Setting up the WordPress side

This is the part everyone underestimates. The Next.js code is the easy half. The WordPress configuration is where the gotchas live.

Get your API in shape

WordPress ships with a REST API out of the box, and for a lot of projects that’s genuinely all you need. Hit /wp-json/wp/v2/posts and you’ve got JSON.

But the default REST API has a real weakness once your data gets relational: over-fetching and round trips. Want a post, its featured image, its author, and its categories? That’s the post object plus more requests to chase the linked resources, or a tangle of _embed params. For a blog index that’s fine. For a product catalog with variations, custom fields, and taxonomies, it gets painful fast.

That’s why on anything non-trivial I install WPGraphQL. I’ll come back to this — it’s the single decision that most changes the developer experience on these builds.

Custom fields and post types

Real client content is never just “posts.” It’s case studies, team members, products, FAQs — structured content with custom fields. I build these out with custom post types and Advanced Custom Fields (ACF).

The key thing people miss: ACF fields don’t automatically appear in your API. You have to expose them. For REST you register the fields with show_in_rest. For GraphQL, the WPGraphQL for ACF add-on surfaces your field groups in the schema. Skip this step and you’ll be staring at a response wondering where your data went — I’ve watched developers lose an afternoon to exactly that.

Permalinks and CORS — the two silent killers

Two configuration things break more headless builds than any code bug:

  1. Permalinks. Set them to “Post name” under Settings → Permalinks. The default plain structure (?p=123) makes the REST API behave strangely. First thing I check when an API endpoint 404s for no reason.
  2. CORS. Your Next.js front-end lives on a different origin than WordPress, so the browser will block requests unless WordPress sends the right headers. For server-side fetching (which is most of what you should be doing — more on that below) this matters less, but the moment you do a client-side fetch from the browser, CORS will bite you. I handle it deliberately rather than slapping * on everything, because allow-all headers on a production API is lazy and insecure.

The Next.js side

Spin up the project:

npx create-next-app@latest my-headless-site
cd my-headless-site
npm run dev

Here’s where I’ll be opinionated, because the old tutorials are now genuinely out of date. Most posts you’ll find (including, honestly, the first version of this one) were written for the Pages Router with getStaticProps. That still works, but if you’re starting fresh today you should be on the App Router. That’s where Next.js is going, and it changes how you fetch data in a way that’s worth understanding.

Fetching content the App Router way

In the App Router, components are Server Components by default. That means you can fetch data directly inside the component — no getStaticProps, no separate data-fetching lifecycle. The fetch runs on the server, the HTML is built there, and the browser gets finished markup.

Here’s the shape of it for a blog index:

// app/blog/page.js
const WP_API = 'https://cms.yoursite.com/wp-json/wp/v2';

async function getPosts() {
  const res = await fetch(`${WP_API}/posts?_embed`, {
    next: { revalidate: 3600 }, // ISR — re-fetch at most once an hour
  });
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <section>
      <h1>From the blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <a href={`/blog/${post.slug}`}>{post.title.rendered}</a>
          </li>
        ))}
      </ul>
    </section>
  );
}

Notice the next: { revalidate: 3600 }. That one line is doing something I think is the real magic of this stack, and it deserves its own section.

ISR: the bit that makes this stack genuinely good

Here’s the problem with pure static generation: your site is fast because it’s pre-built, but the moment an editor publishes a new post, the static site doesn’t know. You’d have to rebuild and redeploy the whole thing. On a 500-page site that’s a non-starter.

Incremental Static Regeneration (ISR) solves this. You set a revalidate window, and Next.js serves the cached static page instantly to every visitor — but in the background, after the window passes, it quietly regenerates that page with fresh content on the next request. Visitors always get a fast static page. Editors get their content live without anyone running a deploy.

This is the answer to the question every client eventually asks: “if it’s static, how does my new blog post show up?” ISR is how. I’ve set up builds where the marketing team publishes ten times a day and never once thinks about deployments — the content just appears within the revalidate window.

For content that must be instant — a price change, a stock update — you can go further with on-demand revalidation, where a WordPress webhook pings a Next.js API route the moment something changes, and that specific page gets purged and rebuilt. That’s the setup I reach for on e-commerce, where stale prices are a real problem.

Dynamic routes for individual posts

// app/blog/[slug]/page.js
const WP_API = 'https://cms.yoursite.com/wp-json/wp/v2';

async function getPost(slug) {
  const res = await fetch(`${WP_API}/posts?slug=${slug}&_embed`, {
    next: { revalidate: 3600 },
  });
  const posts = await res.json();
  return posts[0];
}

export default async function PostPage({ params }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
      <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
    </article>
  );
}

A note on dangerouslySetInnerHTML: WordPress sends rendered HTML, and React makes you opt in to injecting it. In production I don’t trust it blindly — I sanitize that HTML and often parse it so I can swap WordPress’s default <img> and <a> tags for Next.js’s optimized <Image> and <Link> components. That’s the difference between a demo and a real build.

Why WPGraphQL changes the game

I said I’d come back to this. Let me show you why I install it on almost every serious headless project.

With REST, getting a post plus its featured image, author, and categories means either multiple requests or wrestling _embed. With WPGraphQL, you ask for exactly the shape you want in a single query:

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

One request. Exactly the fields you asked for, nothing more. No over-fetching, no chasing linked resources, no four round trips. When your content model gets relational — and real client work always does — this is the difference between code that’s a pleasure to maintain and code that’s a pile of fetch calls held together with hope.

The trade-off is honest: it’s another plugin to keep updated, and it adds a small learning curve if your team only knows REST. But on any build with a non-trivial content model, it pays for itself in the first week.

SEO — because “headless” scares people for the wrong reason

The biggest objection I hear about headless WordPress is “won’t it kill my SEO?” It’s a fair worry, because a badly built React site absolutely can — if everything renders client-side, Google sees an empty page.

But that’s not what we’re building. With server-side rendering and static generation, Google receives fully-formed HTML, the same as it would from a classic WordPress theme. Done right, headless is better for SEO, because the performance gains feed directly into Core Web Vitals.

What I make sure of on every build:

  • Real metadata. The App Router’s generateMetadata function pulls your title, description, and Open Graph tags straight from the WordPress fields (I lean on Yoast or Rank Math’s data, exposed through the API). Every page gets correct, unique meta.
  • Structured data. JSON-LD for articles and products, so Google understands what each page is.
  • Image optimization. Next.js <Image> handles lazy loading, responsive sizing, and modern formats automatically. This is usually the single biggest Core Web Vitals win.
  • A real sitemap and canonical URLs. Generated from the content, not hand-maintained.

Where I’d take it from here

Deployment is the easy finish — push to GitHub, connect Vercel (the team behind Next.js) or Netlify, and you’re live with previews on every commit. The interesting work is everything before that.

On a production build, the things that actually consume my time are the parts the tutorials skip: handling WordPress’s rendered HTML cleanly, wiring on-demand revalidation so editors never wait, securing the API properly, mapping a messy real-world content model into a sane GraphQL schema, and squeezing out the last Core Web Vitals points. That’s the work that separates a weekend demo from something a business can run on.


If you’re weighing up a headless WordPress build — or you’ve got one that isn’t performing the way it should — that’s exactly the kind of problem I spend my days on. I’m a WordPress and WooCommerce developer working on headless architectures, custom plugins, and performance, and I’m happy to talk through whether this stack is the right call for your project. You can see more of my work and get in touch here.

Related Articles