How to Add Live Draft Preview to Headless WordPress
Headless WordPress breaks the editor's "Preview" button by default — it opens a draft URL on the WordPress domain, not the actual Next.js frontend the visitor will see. Fixing it takes two pieces: a WordPress filter that points the Preview button at your Next.js site with a signed token, and a Next.js route that verifies the token, authenticates to WordPress for the unpublished content, and enables Draft Mode. No third-party framework plugin required — here's the vanilla setup.
Why does headless WordPress break preview in the first place?
In classic WordPress, "Preview" just renders the draft through the same theme that renders published posts — nothing special happens. In a headless setup, the frontend is a separate Next.js app that only knows how to fetch published content through the API. WordPress's default Preview link still points at the WordPress domain, so an editor clicking it sees raw WordPress output, not the actual site design. For a content team that lives in the WordPress admin, that's a broken workflow, not a minor inconvenience.
How do you point the Preview button at your Next.js site?
Filter preview_post_link to rewrite the URL WordPress generates for the Preview button. Route it to your Next.js domain with the post ID and a signed token:
add_filter('preview_post_link', function ($link, $post) {
$token = hash_hmac('sha256', $post->ID . $post->post_modified, PREVIEW_SECRET);
return add_query_arg([
'id' => $post->ID,
'token' => $token,
], 'https://yourdomain.com/api/preview');
}, 10, 2);
Tying the token to post_modified means it changes every time the draft is edited and saved — an old preview link stops working once the content underneath it changes, which is a reasonable default rather than a permanent bypass URL floating around in Slack.
How do you build the Next.js route that receives it?
The route verifies the token, then calls Next.js's built-in Draft Mode before redirecting to the actual post path:
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
import crypto from 'crypto';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const token = searchParams.get('token');
const post = await fetchPostById(id); // your existing WP fetch helper
const expected = crypto
.createHmac('sha256', process.env.PREVIEW_SECRET!)
.update(id + post.modified)
.digest('hex');
if (token !== expected) {
return new Response('Invalid preview token', { status: 401 });
}
(await draftMode()).enable();
redirect(`/blog/${post.slug}`);
}
Once Draft Mode is enabled, Next.js skips the static cache for that visitor's session and re-fetches on every request — so the page component needs to know to ask WordPress for the draft version of the content, not the last published one.
How do you actually fetch draft content from WordPress?
This is the part most tutorials skip. The WordPress REST API refuses to return unpublished content to an unauthenticated request — correctly, since a draft might contain unfinished or sensitive copy. Your page component needs to authenticate when Draft Mode is on:
- Create a WordPress Application Password for a dedicated service user (Users → Profile → Application Passwords in wp-admin — built into WordPress core, no plugin needed).
- When rendering with Draft Mode enabled, send that as a Basic Auth header and request
?status=draftinstead of the default published-only query. - Store the Application Password as a server-only environment variable — it must never reach the client bundle.
Without this step, the redirect from the preview route works but the page itself still renders the old published version (or a 404 if there isn't one yet), which is a confusing half-fix.
Custom route vs a framework plugin — which should you use?
| Vanilla custom route (above) | Framework plugin (e.g. HeadstartWP) | |
|---|---|---|
| Setup time | A few hours — two small files, no new dependency | Faster if you're already on that framework's conventions |
| Control | Full — you own the token scheme and the fetch logic | Bounded by the plugin's assumptions about your data shape |
| Best for | Projects with a custom REST/GraphQL layer already in place | Greenfield builds happy to adopt the framework's full opinionated stack |
| Dependency risk | None beyond WordPress core + Next.js itself | Tied to the plugin's release cadence and API stability |
Neither is wrong. The vanilla route is the better fit when the rest of the stack — the fetch layer, the auth, the deployment — is already custom, which is the common case on a project like AzanGuru, where the API layer was purpose-built rather than adopted wholesale from a starter framework.
Common mistakes
- Leaving Draft Mode on. It's a cookie-based session flag — build an exit route (
draftMode().disable()) and link to it from the preview banner, or editors get stuck seeing draft content on every subsequent visit from that browser. - Putting the Application Password in client-side code. It grants read access to unpublished content — treat it exactly like an API secret, server-side only, never in a public environment variable or client fetch.
- Skipping the token expiry tie-in. A static, never-changing preview link is a permanent unauthenticated bypass if it ever leaks. Tying the token to
post_modified(or a short-lived nonce) closes that gap for free. - Forgetting media that only exists on the draft. A featured image uploaded in the same editing session as the draft can 404 if your image-fetching logic also assumes published-only status.
- No visible "you are previewing a draft" banner. Without one, an editor can't tell whether they're looking at the live site or a draft — which defeats the point of a safe preview flow.
Frequently asked questions
Do I need a plugin for WordPress preview in a headless setup?
No. WordPress core already supports everything needed: the preview_post_link filter to redirect the Preview button, and Application Passwords for authenticated draft fetches. A framework plugin like HeadstartWP can speed up setup if you're already using that framework's conventions, but it isn't a requirement.
What is Next.js Draft Mode?
Draft Mode is a built-in Next.js feature (App Router) that lets a specific visitor's session bypass the static cache and force fresh data fetching, without affecting what every other visitor sees. It's enabled per-session via a cookie set by your preview route, and should always have a matching route to disable it again.
Can editors preview a draft without logging into Next.js?
Yes — that's the point of the signed token. The editor clicks Preview inside the WordPress admin they're already logged into; the token in the URL authenticates the preview request without requiring a separate login on the Next.js side.
Is an Application Password safe to use for this?
Yes, as long as it's scoped to a dedicated low-privilege service user and kept server-side only. It's built into WordPress core specifically for this kind of server-to-server authentication, and can be revoked instantly from wp-admin if it's ever compromised.
What happens if the preview token has expired or been tampered with?
The route should reject it outright with a 401 response before ever calling draftMode().enable(). Because the token is tied to the post's last-modified timestamp, editing and re-saving the draft naturally invalidates any previously shared preview link.
Does this work with WPGraphQL instead of the REST API?
Yes — the pattern is the same. WPGraphQL also requires authentication to return draft content, and supports Application Passwords for that. Swap the REST fetch call in the page component for a GraphQL query with the same auth header.
How is this different from just giving editors a staging site login?
A full staging environment shows the entire site as a separate, harder-to-maintain deployment. Draft Mode previews a single unpublished page inside the actual production frontend, using the actual production styling and components — closer to what the editor will really see, with far less infrastructure to keep in sync.
Need this wired into your headless build?
Preview flow is one of the details that separates a headless site editors trust from one they route around. If you're evaluating a headless WordPress build and want it done right the first time, get in touch.