tutorialsWordPressNext.jsHeadless CMS

Headless WordPress with Next.js: Complete Tutorial [2026]

Headless WordPress with Next.js: Complete Tutorial [2026]

Want the best of both worlds? Keep WordPress for content editing while getting Next.js performance? That's exactly what headless WordPress offers.

What is Headless WordPress?

In a traditional WordPress setup, WordPress handles both:

1. Content Management - The admin panel, editor, media library

2. Frontend Rendering - Generating HTML pages for visitors

In a headless setup, WordPress only handles content management. A separate frontend (like Next.js) fetches content via the REST API or GraphQL and renders the pages.

Why Go Headless?

BenefitTraditional WPHeadless WP + Next.js
Page Load Speed2-4 seconds0.5-1 second
SecurityVulnerableMuch safer
ScalabilityLimitedUnlimited
Developer ExperiencePHP/themesReact/TypeScript

Prerequisites

  • WordPress site with REST API enabled (default since WP 4.7)
  • Node.js 18+ installed
  • Basic React/Next.js knowledge

Step 1: Set Up Next.js Project

npx create-next-app@latest my-headless-blog --typescript --tailwind --app

cd my-headless-blog

Step 2: Create WordPress API Helper

Create a file to handle WordPress API calls:

// lib/wordpress.ts

const WP_URL = process.env.WORDPRESS_URL || 'https://your-wp-site.com';

export interface WPPost {

id: number;

slug: string;

title: { rendered: string };

content: { rendered: string };

excerpt: { rendered: string };

date: string;

_embedded?: {

'wp:featuredmedia'?: Array<{

source_url: string;

alt_text: string;

}>;

};

}

export async function getPosts(perPage = 10): Promise {

const res = await fetch(

${WP_URL}/wp-json/wp/v2/posts?per_page=${perPage}&_embed,

{ next: { revalidate: 3600 } }

);

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

return res.json();

}

export async function getPost(slug: string): Promise {

const res = await fetch(

${WP_URL}/wp-json/wp/v2/posts?slug=${slug}&_embed

);

const posts = await res.json();

return posts[0] || null;

}

Step 3: Create Blog Listing Page

// app/blog/page.tsx

import { getPosts } from '@/lib/wordpress';

import Link from 'next/link';

export default async function BlogPage() {

const posts = await getPosts();

return (

Blog

{posts.map((post) => (

/blog/${post.slug}}>

{post.title.rendered}

className="text-gray-600 mt-2"

dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}

/>

))}

);

}

Step 4: Create Dynamic Post Page

// app/blog/[slug]/page.tsx

import { getPost, getPosts } from '@/lib/wordpress';

import { notFound } from 'next/navigation';

import { Metadata } from 'next';

interface Props {

params: { slug: string };

}

export async function generateStaticParams() {

const posts = await getPosts(100);

return posts.map((post) => ({ slug: post.slug }));

}

export async function generateMetadata({ params }: Props): Promise {

const post = await getPost(params.slug);

if (!post) return { title: 'Not Found' };

return {

title: post.title.rendered,

description: post.excerpt.rendered.replace(/<[^>]+>/g, ''),

};

}

export default async function PostPage({ params }: Props) {

const post = await getPost(params.slug);

if (!post) notFound();

return (

{post.title.rendered}

{new Date(post.date).toLocaleDateString()}

className="prose prose-lg mt-8"

dangerouslySetInnerHTML={{ __html: post.content.rendered }}

/>

);

}

Step 5: Enable ISR for Fresh Content

Incremental Static Regeneration keeps content fresh:

// In your fetch calls, add revalidation

{ next: { revalidate: 3600 } } // Revalidate every hour

Best Practices

1. Use Preview Mode

Enable WordPress preview for draft posts:

// app/api/preview/route.ts

export async function GET(request: Request) {

const { searchParams } = new URL(request.url);

const slug = searchParams.get('slug');

// Redirect to the post with preview mode enabled

redirect(/blog/${slug}?preview=true);

}

2. Optimize Images

Use Next.js Image component with WordPress images:

import Image from 'next/image';

src={post._embedded?.['wp:featuredmedia']?.[0]?.source_url}

alt={post._embedded?.['wp:featuredmedia']?.[0]?.alt_text}

width={800}

height={400}

className="rounded-lg"

/>

3. Consider GraphQL

For complex queries, use WPGraphQL plugin:

Install Apollo Client

npm install @apollo/client graphql

When to Go Fully Headless vs Full Migration

ScenarioRecommendation
Non-technical editorsHeadless WordPress
Developer-only teamFull migration to MDX
Complex content relationshipsHeadless WordPress
Simple blogFull migration
Existing WP investmentHeadless first

Conclusion

Headless WordPress with Next.js gives you:

  • ✅ Familiar WordPress editing
  • ✅ Next.js performance
  • ✅ Better security
  • ✅ Modern development workflow

Related guides:

Ready to try it? Start with headless, then consider full migration when you're ready.

Share:

Related Articles

View all

Ready to Migrate Your WordPress Site?

Use our free tool to export your WordPress content in minutes.

Start Free Migration