Headless WordPress + Next.js in 2026: Content API, Preview Mode, Hosting

Network switch with ethernet cables, representing the connection between a WordPress backend and a Next.js frontend

Photo: User_Pascal / Unsplash

Headless WordPress + Next.js in 2026: Content API, Preview Mode, and Where to Host Each Piece

A lot of teams want two things that don't normally come from the same tool: an editor experience non-technical writers already know (WordPress), and frontend performance and control that a template-driven WordPress theme can't give you (Next.js). Running WordPress "headless" — as a content API only, with Next.js rendering the actual site — gets you both, at the cost of a few extra decisions: how to fetch the content, how to preview unpublished drafts safely, and where each half of the stack should actually live.

This guide covers the practical setup, then closes with the hosting question specifically, since a headless project has two separate hosting decisions to make, not one.

What "Headless WordPress" Actually Means

In a traditional WordPress site, WordPress renders the HTML itself via PHP templates. In a headless setup, WordPress keeps doing what it's good at — the editorial dashboard, user roles, media library, revisions — but stops rendering pages. Instead, Next.js fetches content from WordPress over an API and renders the frontend independently. The two systems can even be hosted on completely different infrastructure, which is exactly why the hosting section below treats them as two separate questions.

Two Ways to Pull Content Out of WordPress

WordPress exposes content in two main ways for a headless frontend:

  • The built-in REST API — enabled by default on every modern WordPress install, no plugin required. Endpoints like /wp-json/wp/v2/posts return JSON out of the box.
  • WPGraphQL — a popular plugin that adds a GraphQL endpoint, letting you request exactly the fields you need in a single query instead of over-fetching whole REST objects. It requires installing and maintaining a plugin on the WordPress side, which is a real maintenance cost worth weighing against the query-efficiency benefit.

The REST API is the lower-friction starting point since it needs zero WordPress-side setup. WPGraphQL is worth adopting once your content model gets complex enough that you're fetching and discarding a lot of unused fields with REST.

// app/blog/page.tsx — fetching posts via the built-in REST API
async function getPosts() {
  const res = await fetch('https://cms.example.com/wp-json/wp/v2/posts?_embed', {
    next: { revalidate: 3600, tags: ['posts'] },
  })

  if (!res.ok) {
    throw new Error('Failed to fetch posts')
  }

  return res.json()
}

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

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>
          <a href={`/blog/${post.slug}`}>{post.title.rendered}</a>
        </li>
      ))}
    </ul>
  )
}

The _embed query parameter pulls in related data (featured image, author, categories) in the same request, avoiding extra round trips. The next: { revalidate, tags } options use Next.js's built-in fetch caching — data is cached for the given number of seconds and can also be invalidated on demand by tag, which is what the webhook section below relies on.

On-Demand Revalidation: Publishing Without Waiting for a Rebuild

Time-based revalidation alone means an editor might publish a post and have it sit invisible for up to an hour. The fix is on-demand revalidation: WordPress calls a Next.js Route Handler the moment a post is published or updated, and that handler tells Next.js to refresh just that content.

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const secret = request.headers.get('x-revalidate-secret')

  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }

  revalidateTag('posts')

  return Response.json({ revalidated: true, now: Date.now() })
}

This endpoint follows the same rule covered in the API security article in this series: never trust an incoming request just because it arrived at the right URL. The secret check here is the entire security model for this route — without it, anyone who finds the URL could trigger revalidation (a low-severity but real abuse vector) or, if the handler were ever expanded to accept more input, worse. On the WordPress side, a lightweight webhook plugin (or a few lines added to a custom plugin's save_post hook) sends this header on every publish/update. VERIFY BEFORE PUBLISHING: confirm the current recommended webhook plugin or hook pattern against the WordPress Plugin Developer Handbook at publish time, since specific plugin recommendations change.

Preview Mode: Letting Editors See Drafts Safely

Editors need to see unpublished drafts before they go live. Next.js's Draft Mode is built for exactly this — it sets a cookie that, when present, tells your data-fetching code to bypass the cache and pull the latest (possibly unpublished) content.

// app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')

  if (secret !== process.env.PREVIEW_SECRET || !slug) {
    return Response.json({ error: 'Invalid preview request' }, { status: 401 })
  }

  const draft = await draftMode()
  draft.enable()

  redirect(`/blog/${slug}`)
}

Same principle as the revalidation route: the secret check is what stands between "authorized editor preview" and "anyone with the URL can see unpublished drafts" — this is functionally a lightweight authentication check on a single-purpose endpoint, the same pattern covered in more depth in the authentication article in this series. VERIFY BEFORE PUBLISHING: confirm whether draftMode() requires await in the exact Next.js version you're running — recent versions moved several next/headers APIs to be asynchronous, and this is exactly the kind of version-dependent detail worth double-checking against the current Next.js docs before shipping.

Rendering WordPress Content Safely

WordPress post content comes back as an HTML string, and the common pattern for displaying it is dangerouslySetInnerHTML — which is exactly as risky as the name implies if the content source isn't trusted. For content written by your own editorial team through the WordPress dashboard, this is a reasonable, standard pattern. If your setup allows any untrusted or public submission path into that content (comments rendered as HTML, guest contributor accounts, user-generated fields), sanitize the HTML server-side before rendering — this is the same category of concern the API security article covers under input validation, just applied to content instead of form input.

// app/blog/[slug]/page.tsx
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPostBySlug(slug)

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

Handling Images from WordPress's Media Library

next/image requires explicitly allow-listing external image domains before it will optimize them — WordPress media URLs will fail to load until your WordPress domain is added:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cms.example.com',
        pathname: '/wp-content/uploads/**',
      },
    ],
  },
}

export default nextConfig

SEO: Metadata Driven by WordPress Data

Since content lives in WordPress, page metadata should be generated from it rather than hardcoded, using Next.js's generateMetadata function:

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPostBySlug(slug)

  return {
    title: post.title.rendered,
    description: post.excerpt.rendered.replace(/<[^>]*>/g, '').slice(0, 160),
  }
}

Where to Host Each Piece

A headless project has two hosting decisions, and they don't have to be the same provider — in fact, they usually aren't.

The Next.js Frontend

This is the same hosting decision covered in this blog's existing cloud deployment guide — the considerations there (build/serverless function support, edge caching, deployment workflow) apply directly here and aren't repeated in this article.

The WordPress Backend

This half of the stack is ordinary WordPress — the same editorial dashboard, plugins, and PHP runtime as a traditional WordPress site, just with its theme doing nothing more than serving the REST API (or sitting alongside WPGraphQL). That means it needs the same thing any WordPress install needs: a managed PHP/WordPress host that handles server maintenance, backups, SSL, and WordPress-specific performance tuning, so you're not hand-rolling server administration for what is, from WordPress's perspective, a completely standard install.

Cloudways is a fit specifically for this half of the stack — it's a managed hosting platform built around PHP applications including WordPress, with one-click server provisioning across infrastructure providers like DigitalOcean, AWS, and Google Cloud, plus automatic backups, staging environments, and free SSL. Worth being precise about what it is and isn't here: it's a managed host for the WordPress/PHP backend, not a Next.js-native deployment target the way Vercel is — don't expect a one-click Next.js build pipeline from it. If your WordPress backend is going to sit somewhere for the long term getting real editorial traffic, that's exactly the use case managed WordPress hosting is built for. VERIFY BEFORE PUBLISHING: confirm current Cloudways plans and pricing directly on their site before publishing, since this article intentionally doesn't state specific numbers.

Common Mistakes

  • Relying only on time-based revalidate and wondering why published posts take up to an hour to appear — add on-demand revalidation instead of shortening the interval.
  • Leaving the revalidation or preview endpoint without a secret check, effectively making cache invalidation or draft access public.
  • Forgetting to allow-list the WordPress media domain in next.config.ts, causing every featured image to silently fail to load.
  • Treating REST and WPGraphQL as interchangeable mid-project — switching later means rewriting every data-fetching function, so it's worth deciding early.
  • Hardcoding page metadata instead of generating it from the WordPress content that's supposed to be the source of truth.

Practical Checklist

  • ☐ Decided between the built-in REST API and WPGraphQL based on how complex the content model actually is.
  • ☐ On-demand revalidation endpoint exists and checks a shared secret before invalidating anything.
  • ☐ Preview/draft mode endpoint checks a shared secret before enabling draft cookies.
  • ☐ WordPress content rendered via dangerouslySetInnerHTML comes only from trusted editorial sources, or is sanitized first.
  • ☐ WordPress media domain is allow-listed in next.config.ts.
  • ☐ Page metadata is generated from WordPress content, not hardcoded.
  • ☐ Frontend and backend hosting decisions were made separately, each for what that piece actually needs.

The technical pieces here — a secret-checked webhook, a secret-checked preview route, sanitizing content before rendering it — are the same authentication and authorization discipline covered throughout this series, just applied to a content pipeline instead of user accounts. Get WordPress out of the rendering business, keep Next.js honest about what it trusts, and host each half where it actually belongs.


Related reading:

This article contains an affiliate link to Cloudways. If you sign up through it, this blog may earn a commission at no extra cost to you.

Comments

Popular posts from this blog

Why Python is Still the King of AI Programming in 2026: A Deep Dive

The AI Revolution in Full Stack Development: 2026 Comprehensive Guide

Top 5 AI Automation Tools Every Developer Must Use in 2026