The preview problem in headless
In a traditional WordPress architecture, the "Preview" button generates an HTML page via the active PHP theme. The editor sees the final rendering directly in their browser. In a headless architecture, the PHP theme is no longer used: rendering is handled by the Next.js frontend. WordPress's native "Preview" button therefore displays an empty page or an error message.
This issue is one of the main pain points reported by editorial teams migrating to a headless architecture. The solution relies on Next.js's Draft Mode, combined with a preview webhook configured in WordPress.
Next.js Draft Mode
Draft Mode is a built-in Next.js mechanism that allows you to bypass the static cache to display unpublished content. When enabled, pages are generated on the fly (like SSR) instead of being served from the cache.
How it works
- The editor clicks "Preview" in WordPress
- WordPress redirects to a Next.js frontend API route with a security token
- The API route enables Draft Mode by setting a
__prerender_bypasscookie in the browser - The browser is redirected to the article page
- Next.js detects the cookie and generates the page in real time, fetching the draft via the WordPress API
Implementing the Draft Mode route
Create the preview API route
This route receives the request from WordPress, verifies the security token, enables Draft Mode, and redirects to the corresponding page.
// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
const slug = request.nextUrl.searchParams.get('slug');
const postType = request.nextUrl.searchParams.get('type') ?? 'post';
// Security token verification
if (secret !== process.env.DRAFT_SECRET_TOKEN) {
return new Response('Invalid token', { status: 401 });
}
if (!slug) {
return new Response('Missing slug', { status: 400 });
}
// Verify that the content exists in WordPress
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/${postType}s?slug=${slug}&status=draft`,
{
headers: {
Authorization: `Bearer ${process.env.WORDPRESS_API_TOKEN}`,
},
}
);
const posts = await res.json();
if (!posts.length) {
return new Response('Content not found', { status: 404 });
}
// Enable Draft Mode
draftMode().enable();
// Redirect to the page
const path = postType === 'page' ? `/${slug}` : `/blog/${slug}`;
redirect(path);
}
Adapt the page component for Draft Mode
When Draft Mode is active, the page must fetch the draft (status=draft) instead of the published content.
// app/blog/[slug]/page.tsx
import { draftMode } from 'next/headers';
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const { isEnabled } = draftMode();
// In draft mode: fetch the draft with authentication
// In normal mode: fetch the published content
const statusParam = isEnabled ? '&status=draft' : '';
const headers: HeadersInit = isEnabled
? { Authorization: `Bearer ${process.env.WORDPRESS_API_TOKEN}` }
: {};
const res = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?slug=${params.slug}${statusParam}&_embed`,
{
headers,
cache: isEnabled ? 'no-store' : undefined,
next: isEnabled ? undefined : { revalidate: 3600 },
}
);
const posts = await res.json();
const post = posts[0];
return (
<article>
{isEnabled && (
<div style={{ background: '#fef3c7', padding: '12px', marginBottom: '24px' }}>
Preview mode active —{' '}
<a href="/api/draft/disable">Disable</a>
</div>
)}
<h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</article>
);
}
Create the route to disable Draft Mode
Lets the editor exit preview mode.
// app/api/draft/disable/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET() {
draftMode().disable();
redirect('/');
}
Configure WordPress to redirect to the frontend
In WordPress, change the preview URL to point to the frontend API route. This code goes in the theme's functions.php file or in a custom plugin.
// functions.php or custom plugin
function headless_preview_link($preview_link, $post) {
$frontend_url = 'https://www.mysite.com';
$secret = 'YOUR_SECRET_TOKEN'; // Same as DRAFT_SECRET_TOKEN
$slug = $post->post_name;
$type = $post->post_type;
return sprintf(
'%s/api/draft?secret=%s&slug=%s&type=%s',
$frontend_url,
$secret,
$slug,
$type
);
}
add_filter('preview_post_link', 'headless_preview_link', 10, 2);
Editorial workflow in headless
Editorial workflow organization does not depend on the technical architecture. WordPress keeps its native publication statuses: draft, pending review, published, scheduled. The difference in headless lies in how each status is made visible to the editor.
Content lifecycle
Roles and permissions
WordPress has a native role system that applies in headless mode:
- Administrator: full access to WordPress and API settings
- Editor: can publish, modify, and delete all content
- Author: can publish and modify their own content
- Contributor: can create drafts but cannot publish
Separate publication rights from authoring rights
For teams of more than three editors, it is recommended to restrict publication rights to editors. Authors submit articles in "Pending Review" status and an editor approves them after checking the content, SEO metadata, and frontend preview.
Revision management
WordPress automatically saves a revision on each save. In headless mode, these revisions are accessible via the REST API:
# Fetch revisions of a post (requires authentication)
GET /wp-json/wp/v2/posts/{id}/revisions
To prevent revisions from accumulating in the database, limit their number in wp-config.php:
// wp-config.php — Limit to 10 revisions per post
define('WP_POST_REVISIONS', 10);
Comparing revisions
The WordPress admin interface lets you visually compare two revisions. In headless, this feature remains available in the back-office. To preview a specific revision on the frontend, extend the Draft Mode route:
// Addition in the /api/draft route to support revisions
const revisionId = request.nextUrl.searchParams.get('revision');
if (revisionId) {
const revisionRes = await fetch(
`${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts/${postId}/revisions/${revisionId}`,
{ headers: { Authorization: `Bearer ${process.env.WORDPRESS_API_TOKEN}` } }
);
// Use the revision content for rendering
}
Scheduled publication
WordPress lets you schedule a post for a future date and time. In headless mode with ISR, the behavior depends on the revalidation strategy:
For scheduled publication with on-demand revalidation:
// WordPress plugin — Webhook fired on publication
function trigger_revalidation($post_id, $post) {
if ($post->post_status !== 'publish') return;
$frontend_url = 'https://www.mysite.com';
$secret = 'YOUR_REVALIDATION_SECRET';
wp_remote_post($frontend_url . '/api/revalidate?secret=' . $secret, [
'body' => json_encode([
'post_name' => $post->post_name,
'post_type' => $post->post_type,
]),
'headers' => ['Content-Type' => 'application/json'],
]);
}
add_action('publish_post', 'trigger_revalidation', 10, 2);
Multi-author collaboration
For editorial teams of multiple people, the following recommendations make collaboration easier in headless:
- Use custom statuses: add intermediate statuses (e.g., "In translation", "Legal review") with the PublishPress plugin or via custom code
- Enable notifications: configure automatic emails on status changes (draft to review, review to publication)
- Centralize SEO metadata: use Yoast SEO or Rank Math so each author fills in metadata directly in the back-office
- Document conventions: maintain an editorial style guide accessible from the WordPress back-office
Staging environment
A staging environment (pre-production) is recommended to validate changes before going live.
Recommended setup
-
WordPress staging: a copy of WordPress with its own database, periodically synced from production
-
Frontend staging: a Next.js instance connected to the staging WordPress, deployed on a dedicated URL (e.g., staging.mysite.com)
-
Environment variables: each environment uses its own variables (
NEXT_PUBLIC_WORDPRESS_URL,DRAFT_SECRET_TOKEN) -
Workflow: authoring on staging → preview → validation → sync to production → publication
This setup lets you test content and design changes without affecting the live site.
Key takeaways
Best practices summary
- Next.js's Draft Mode is the standard mechanism for preview in headless. It bypasses the static cache and renders the page in real time with the draft content.
- WordPress configuration (
preview_post_linkfilter) is essential so the "Preview" button redirects to the frontend. - The editorial workflow (draft → review → publication) works identically in headless. Only preview requires extra configuration.
- On-demand revalidation is recommended for scheduled publications to ensure time precision.
Managing content easily in headless mode
Article suivantAuthentication and session management