Headless WordPress with Next.js: Complete Tutorial [2026]
Asad Ali
Founder & Lead Developer · Former WordPress Core Contributor
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?
| Benefit | Traditional WP | Headless WP + Next.js |
| Page Load Speed | 2-4 seconds | 0.5-1 second |
| Security | Vulnerable | Much safer |
| Scalability | Limited | Unlimited |
| Developer Experience | PHP/themes | React/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
Scenario Recommendation
Non-technical editors Headless WordPress
Developer-only team Full migration to MDX
Complex content relationships Headless WordPress
Simple blog Full migration
Existing WP investment Headless 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.
Related Articles
View all Main GuidetutorialWordPress to Next.js Migration: The Complete 2026 Guide
Everything you need to know about migrating from WordPress to Next.js. From planning to deployment, this guide covers the entire migration process.
18 min read tutorialWordPress REST API: A Complete Guide for Developers
Master the WordPress REST API. Understand endpoints, authentication, custom routes, and how this enables headless WordPress.
14 min read Main GuidecomparisonHow to Choose a Headless CMS in 2026: Decision Guide
Navigate the headless CMS landscape with this comprehensive decision guide. Compare Sanity, Contentful, Strapi, Payload, and more.
15 min read tutorialNext.js App Router: Ultimate Guide for WordPress Developers
Learn Next.js App Router coming from WordPress. Server components, layouts, data fetching, and building real applications.
15 min read
Ready to Migrate Your WordPress Site?
Use our free tool to export your WordPress content in minutes.
Start Free Migration