So, you’re building a sleek Next.js frontend and hooking it up to a robust WordPress backend. Fantastic! But then you hit a familiar snag: how do you get those “preview” buttons in WordPress to actually show you what your content will look like before it’s published on your Next.js site? Don’t worry, it’s a common challenge, but entirely solvable.
The short answer is: you need a mechanism that allows WordPress to communicate a draft state to your Next.js application, enabling Next.js to fetch and render that unpublished content. This usually involves generating a unique token or secret within WordPress, passing it to Next.js via a specific URL structure, and then Next.js using that token to request the draft data from WordPress’s API.
Let’s dive a bit deeper into the practicalities.
When we talk about a “decoupled” or “headless” WordPress setup, we mean that WordPress is primarily acting as a content management system (CMS), providing data through an API (like REST or GraphQL), and a separate application (our Next.js site) is responsible for rendering that data.
The standard WordPress “Preview” button, by default, tries to load the WordPress theme’s preview page. But in our decoupled world, there’s no WordPress theme rendering the frontend. Next.js is doing that. So, we need to bridge this gap.
The fundamental issue is that unpublished content isn’t typically available to the public. If it were, anyone could stumble upon your half-finished blog posts. So, the preview mechanism needs to be secure and temporary, providing a window into content that’s not yet live.
Why Standard Previews Don’t Work Natively
- No WordPress Theme: Your Next.js site is your frontend. WordPress doesn’t know how to “preview” on that.
- Public vs. Private: Unpublished posts are usually private. How do you tell Next.js to fetch something secret without making it entirely public?
- API Exposure: WordPress’s standard REST API, for instance, might not expose draft content without specific authentication or parameters.
If you’re looking to enhance your understanding of managing WordPress previews in a decoupled Next.js frontend, you might find it beneficial to explore a related article that delves into the integration of headless CMS with modern JavaScript frameworks. This resource provides insights on optimizing content delivery and improving user experience. For more information, check out this article: here.
The Essential Ingredients for a Next.js Preview System
To make this work, you’ll generally need a few key components playing nicely together. Think of it like a little network of secure messengers.
WordPress: The Content Originator
On the WordPress side, you’ll need a way to:
- Generate a Preview Token/Secret: This acts as a temporary key that grants access to the draft content. It should be unique, hard to guess, and ideally expire after a certain time or use.
- Modify the Preview Link: When an editor clicks “Preview,” WordPress needs to generate a URL pointing to your Next.js site, not its own, and include the preview token and the ID of the content being previewed.
- Expose Draft Content: Your WordPress API (whether REST, GraphQL, or a custom endpoint) needs to be configured to return draft content when presented with the correct preview token.
Next.js: The Content Presenter
On your Next.js application, you’ll need to:
- Handle Incoming Preview Requests: A specific route in your Next.js app needs to be set up to catch the preview links generated by WordPress.
- Securely Fetch Draft Data: When a preview request comes in, Next.js needs to extract the preview token and content ID, then use them to make a secure API call back to WordPress to get the draft version of the content.
- Display the Draft: Once the draft data is fetched, Next.js renders it just like any other content, but perhaps with a visual indicator that it’s a preview.
- Clear Preview State: Provide a way to exit the preview mode and return to the live content view.
Method 1: Using WordPress’s Built-in API and Next.js Draft Mode
This is often the most straightforward and recommended approach, leveraging Next.js’s native Draft Mode feature (formerly Preview Mode).
1. Setting Up WordPress for Previews
We need a way for WordPress to generate a special link and provide access to draft posts.
a. Customizing the Preview Button Link
WordPress’s default preview link points to /?p=[post_id]&preview=true. We need to override this.
You’ll typically do this with a filter in your WordPress functions.php file or a custom plugin. The goal is to generate a URL like https://yournextjsdomain.com/api/preview?secret=[YOUR_SECRET_KEY]&slug=[post_slug]&post_type=[post_type_slug].
“`php
function custom_preview_link($link, $post) {
if (is_preview() || !is_object($post) || !isset($post->post_type)) {
return $link;
}
// Replace with your actual Next.js application URL
$nextjs_base_url = ‘https://yournextjsdomain.com’;
$preview_api_route = ‘/api/preview’; // The Next.js API route we’ll create
// Get a secret key. This should be a strong, hard-to-guess string.
// Store it securely, perhaps in your wp-config.php or as an environment variable.
// Make sure this matches the secret key in your Next.js application.
$secret = get_option(‘NEXTJS_PREVIEW_SECRET’, ‘YOUR_SUPER_SECRET_KEY_HERE’);
// Build the query parameters for your Next.js route
$query_params = array(
‘secret’ => $secret,
‘slug’ => $post->post_name, // Or use a custom field for slug if needed
‘post_type’ => $post->post_type,
// You might want to pass the post ID too for more robust fetching
‘id’ => $post->ID,
);
// Build the full URL
$new_preview_link = add_query_arg($query_params, $nextjs_base_url . $preview_api_route);
return $new_preview_link;
}
add_filter(‘preview_post_link’, ‘custom_preview_link’, 10, 2);
// Optionally, you might need to adjust the “View Post” link in the admin bar too
function custom_wp_admin_bar_view_site_link($wp_admin_bar) {
global $post;
if (!is_admin() && is_singular() && is_object($post) && isset($post->post_status) && $post->post_status === ‘publish’) {
// This filter is primarily for published posts, need to adapt if viewing draft
// The above preview_post_link filter should handle the actual preview button
}
}
// add_action( ‘wp_admin_bar_menu’, ‘custom_wp_admin_bar_view_site_link’, 80 ); // Be careful with this, might override other things.
“`
Important: Don’t hardcode YOUR_SUPER_SECRET_KEY_HERE in production. Fetch it from an environment variable or wp-config.php.
b. Exposing Draft Content via API
The standard REST API wp/v2/posts endpoint doesn’t expose drafts unless you’re authenticated. You can handle this in a few ways:
- OAuth or Application Passwords: If your Next.js app can securely send authentication headers to WordPress (e.g., using an application password created in WordPress settings), you can query drafts directly. This is more complex for previews as the browser client would need those credentials.
- Custom REST Endpoint with Token Check: A safer approach is to create a custom REST API endpoint in WordPress that, only when presented with your shared secret, allows fetching of draft content for a specific post ID or slug.
“`php
// In functions.php or a custom plugin
add_action(‘rest_api_init’, function () {
register_rest_route(‘myplugin/v1’, ‘/preview-post’, array(
‘methods’ => ‘GET’,
‘callback’ => ‘myplugin_get_preview_post’,
‘args’ => array(
‘id’ => array(
‘required’ => true,
‘validate_callback’ => function($param, $request, $key) {
return is_numeric($param);
}
),
‘secret’ => array(
‘required’ => true,
‘validate_callback’ => function($param, $request, $key) {
return is_string($param);
}
)
),
‘permission_callback’ => function ($request) {
// Retrieve your secret key securely (e.g., from an environment variable)
$expected_secret = get_option(‘NEXTJS_PREVIEW_SECRET’, ‘YOUR_SUPER_SECRET_KEY_HERE’);
return $request[‘secret’] === $expected_secret;
}
));
});
function myplugin_get_preview_post($request) {
$post_id = intval($request[‘id’]);
$post = get_post($post_id);
if (!$post || ($post->post_status !== ‘draft’ && $post->post_status !== ‘pending’ && $post->post_status !== ‘future’ && $post->post_status !== ‘private’)) {
return new WP_Error(‘rest_no_preview_post’, ‘No draft post found with that ID or status.’, array(‘status’ => 404));
}
// You might need to process the post data similar to how your main API does
// For example, if you rely on WPGraphQL, you’d replicate some of that logic here
$data = array(
‘id’ => $post->ID,
‘slug’ => $post->post_name,
‘title’ => $post->post_title,
‘content’ => apply_filters(‘the_content’, $post->post_content), // Apply content filters
‘status’ => $post->post_status,
‘date’ => $post->post_date,
// Add other custom fields/ACF data here
);
return new WP_REST_Response($data, 200);
}
“`
This custom endpoint provides a secure gateway to draft content.
2. Configuring Next.js for Draft Mode
Next.js provides a “Draft Mode” (formerly “Preview Mode”) that’s perfect for this.
a. Creating the Next.js API Route for Preview Activation
This API route will receive the request from your WordPress preview link.
Create pages/api/preview.js:
“`javascript
// pages/api/preview.js
export default async function preview(req, res) {
// 1. Validate the secret
const { secret, slug, id, post_type } = req.query;
// Replace with your actual environment variable for the secret
// Make sure this matches the secret generated in WordPress
const NEXTJS_PREVIEW_SECRET = process.env.NEXTJS_PREVIEW_SECRET;
if (!secret || secret !== NEXTJS_PREVIEW_SECRET || (!slug && !id)) {
return res.status(401).json({ message: ‘Invalid token or missing slug/ID’ });
}
// 2. Fetch the draft post data from WordPress
// You’d use the ID or slug here to fetch the specific draft post
// from your custom WordPress REST endpoint or WPGraphQL setup.
// Example using the custom REST endpoint we defined:
let postData;
try {
const wordpressApiBase = process.env.WORDPRESS_API_URL; // e.g., ‘https://yourwordpressdomain.com/wp-json’
const previewEndpoint = ${wordpressApiBase}/myplugin/v1/preview-post?id=${id}&secret=${secret}; // Or slug=${slug}
const response = await fetch(previewEndpoint);
if (!response.ok) {
throw new Error(WordPress API error: ${response.statusText});
}
postData = await response.json();
if (!postData) {
return res.status(404).json({ message: ‘Post not found.’ });
}
} catch (error) {
console.error(‘Error fetching preview post:’, error);
return res.status(500).json({ message: ‘Error fetching preview post data.’ });
}
// 3. Set Next.js Draft Mode cookies
res.setDraftMode({ enable: true });
// 4. Redirect to the content page
const redirectPath = /${post_type || 'post'}/${postData.slug || slug}; // Adjust based on your routing
res.redirect(redirectPath);
}
“`
Environment Variables: Make sure you set NEXTJS_PREVIEW_SECRET and WORDPRESS_API_URL in your .env.local file (and production environment variables).
b. Handling Draft Mode in Your Next.js Pages
In your [slug].js (or similar) content page, you’ll need to adapt getStaticProps or getServerSideProps to check if draft mode is active.
“`javascript
// pages/[postType]/[slug].js (or adjust your dynamic route)
import { draftMode } from ‘next/headers’;
import { notFound } from ‘next/navigation’;
export async function generateStaticParams() {
// Fetch all published post slugs to pre-render static pages
// This function doesn’t need to consider drafts, as drafts are dynamic requests
const publishedPosts = await fetch(${process.env.WORDPRESS_API_URL}/wp/v2/posts?per_page=100)
.then(res => res.json());
return publishedPosts.map(post => ({
postType: ‘posts’, // Or whatever your post type slug is
slug: post.slug,
}));
}
export default async function Page({ params }) {
const { isEnabled } = draftMode();
const { postType, slug } = params;
let post;
if (isEnabled) {
// If in Draft Mode, dynamically fetch the latest (draft) content
// Use the secret from your environment (only available server-side)
const NEXTJS_PREVIEW_SECRET = process.env.NEXTJS_PREVIEW_SECRET;
// Using our custom WordPress preview endpoint
const previewEndpoint = ${process.env.WORDPRESS_API_URL}/myplugin/v1/preview-post?slug=${slug}&secret=${NEXTJS_PREVIEW_SECRET};
try {
const response = await fetch(previewEndpoint, { cache: ‘no-store’ }); // Don’t cache draft content
if (!response.ok) {
throw new Error(Failed to fetch draft post: ${response.statusText});
}
post = await response.json();
} catch (error) {
console.error(“Error fetching draft post:”, error);
notFound(); // Fallback if draft can’t be fetched
}
} else {
// If NOT in Draft Mode, fetch the published content (can be static/ISR)
const publishedEndpoint = ${process.env.WORDPRESS_API_URL}/wp/v2/posts?slug=${slug}&_embed;
try {
const response = await fetch(publishedEndpoint);
if (!response.ok) {
throw new Error(Failed to fetch published post: ${response.statusText});
}
const data = await response.json();
post = data[0]; // REST API returns an array
} catch (error) {
console.error(“Error fetching published post:”, error);
notFound();
}
}
if (!post) {
notFound();
}
return (
{isEnabled &&
}
{post.title.rendered || post.title}
{/ Handle different title structures /}
{/ Handle different content structures /}{/ … other post details /}
);
}
“`
Key Points for Draft Mode:
draftMode().isEnabled: This boolean tells you if the client is currently in draft mode.cache: 'no-store': Crucial for preview content to ensure you always get the latest draft, not a cached version.- Dynamic Data Fetching: When in draft mode, you’ll need to fetch the post data dynamically, typically during the request (e.g., in
getServerSidePropsor directly within the component if it’s a server component). - Security of Secrets: The
NEXTJS_PREVIEW_SECRETshould never be exposed to the client-side. It should only be used in your Next.js API route and server-side data fetching functions.
3. Exiting Preview Mode
You’ll need a way for editors to easily exit preview mode.
a. Clear Preview Cookies API Route
Create pages/api/exit-preview.js:
“`javascript
// pages/api/exit-preview.js
import { draftMode } from ‘next/headers’;
export default async function exit(req, res) {
res.setDraftMode({ enable: false });
res.redirect(‘/’); // Redirect to the homepage or the original post if you know it
}
“`
b. Adding an “Exit Preview” Link/Button
You can add a UI element to your Next.js site that links to /api/exit-preview. This is especially useful in the preview banner.
“`javascript
function PreviewBanner({ isEnabled }) {
if (!isEnabled) return null;
return (
);
}
// Then in your layout or page:
//
“`
Method 2: Custom Preview Endpoints (Less Common with Next.js)
Before Next.js introduced Draft Mode, or if you’re not using Next.js, folks often rolled their own. This involves more manual cookie management and state handling.
1. WordPress: Generate Token and Redirect
This part is similar to Method 1, overriding the preview link with a secret. The key difference is that instead of activating Next.js’s native Draft Mode, you’d be setting your own cookie or state.
2. Next.js: Custom API Route to Set a Preview Cookie
Instead of res.setDraftMode, your API route would set a custom cookie like res.setHeader('Set-Cookie', 'my_preview_token=abc; Path=/; Max-Age=3600');.
3. Next.js: Read Cookie and Fetch Draft Data
On your content pages, you’d manually check for this custom cookie on each request (via getServerSideProps or getInitialProps) and, if present, use its token to fetch draft data.
This approach is more work and less integrated than Next.js’s Draft Mode, so I generally advise against it for Next.js projects unless you have very specific, complex requirements that Draft Mode can’t meet.
When working with a decoupled Next.js frontend, managing WordPress previews can be a bit challenging, but there are effective strategies to streamline the process. For a deeper understanding of how to implement these strategies, you might find it helpful to explore a related article that discusses payment integration in Next.js applications. This resource provides insights that can enhance your overall development experience. You can read more about it in this article on payment integration.
Method 3: Using WPGraphQL and wp-graphql-for-acf
If you’re using WPGraphQL, this approach offers a very clean way to handle previews. WPGraphQL natively supports preview as an argument for nodes, even without extra plugins for simple post/page data. For ACF content, you’ll typically need wp-graphql-for-acf.
1. WordPress: WPGraphQL Setup
Assuming you have WPGraphQL and wp-graphql-for-acf configured:
a. Customizing the Preview Link (still needed)
The WordPress filter preview_post_link will still be used as in Method 1 to redirect to your Next.js application’s API route. The secret and id (or slug) parameters are still crucial.
b. WPGraphQL for Drafts
WPGraphQL, when authenticated, can easily return drafts. The key is in how your Next.js app sends the request to WPGraphQL. For Next.js Draft Mode, the isEnabled flag means you can send a more privileged query to WPGraphQL.
2. Next.js: GraphQL Query with Preview Access
a. API Route (Same as Method 1)
Your pages/api/preview.js will remain largely the same, setting res.setDraftMode({ enable: true }) and redirecting.
b. Content Page with WPGraphQL Queries
In your [slug].js page, the GraphQL query itself will change when in draft mode.
“`javascript
// pages/[postType]/[slug].js
import { draftMode } from ‘next/headers’;
import { notFound } from ‘next/navigation’;
// Assume you have a GraphQL client setup, e.g., using ‘graphql-request’ or Apollo
async function fetchGraphQL(query, { variables = {}, headers = {} } = {}) {
const res = await fetch(${process.env.WORDPRESS_GRAPHQL_URL}, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
// Add authentication header if your WPGraphQL requires it for drafts
// For instance, if using Application Passwords with wp-graphql-jwt-authentication
// ‘Authorization’: Bearer ${process.env.WP_GRAPHQL_AUTH_TOKEN},
…headers
},
body: JSON.stringify({
query,
variables,
}),
cache: ‘no-store’, // Crucial for drafts
});
const json = await res.json();
if (json.errors) {
console.error(json.errors);
throw new Error(‘Failed to fetch GraphQL data’);
}
return json.data;
}
export default async function Page({ params }) {
const { isEnabled } = draftMode();
const { postType, slug } = params;
// Define your base GraphQL query. For preview, we’ll alter it.
const query = `
query GetPostBySlug($slug: String!, $asPreview: Boolean = false, $id: ID) {
postBy(slug: $slug, id: $id, idType: DATABASE_ID, asPreview: $asPreview) {
id
databaseId
slug
title
date
content
status
If using ACF
acf {
someCustomField
}
}
}
`;
let postData;
try {
const data = await fetchGraphQL(query, {
variables: {
slug: slug,
asPreview: isEnabled, // This is the key for WPGraphQL!
// If your preview link passes ‘id’, you can use idType: DATABASE_ID for more reliable fetching
// id: isEnabled ? params.id : null,
},
// You might need to send a temporary token/cookie for WPGraphQL preview
// If your WPGraphQL setup uses JWT for previews, this header would be needed.
headers: isEnabled ? { ‘x-wp-nonce’: process.env.WP_PREVIEW_NONCE } : {} // Example with Nonce
});
postData = data.postBy;
} catch (error) {
console.error(“Error fetching post data:”, error);
notFound();
}
if (!postData) {
notFound();
}
return (
{isEnabled &&
}
{postData.title}
{/ … /}
);
}
export async function generateStaticParams() {
// Fetch slugs for published posts for static generation.
// Drafts are handled dynamically.
const data = await fetchGraphQL(`
query AllPostsSlugs {
posts(first: 100) {
nodes {
slug
}
}
}
`);
return data.posts.nodes.map((post) => ({
postType: ‘posts’, // Or whatever your post type slug is
slug: post.slug,
}));
}
“`
WPGraphQL Specifics:
asPreview: Boolean: This is the powerful argument forpostBy,pageBy, etc. Whentrue, it tells WPGraphQL to fetch the latest revision, including drafts.- Authentication for
asPreview: To useasPreview: true, your GraphQL request from Next.js (when in preview mode) must be authenticated with a WordPress nonce or a JWT token that has edit capabilities. - Nonce: You can generate a WordPress nonce on the server side and pass it as a header (e.g.,
x-wp-nonce). This nonce would need to be passed into your Next.js API route. - JWT (with
wp-graphql-jwt-authentication): If you’re using this plugin, your Next.js backend can authenticate as a user with appropriate permissions and send a JWT token with the GraphQL request. cache: 'no-store': Still essential for ensuring fresh draft data.
If you’re exploring ways to enhance your WordPress previews in a decoupled Next.js frontend, you might find it helpful to read a related article that discusses best practices for integrating headless CMS solutions with modern frameworks. This resource provides insights into optimizing content delivery and improving user experience. For more information, you can check out the article on best practices for headless CMS integration.
Common Pitfalls and Troubleshooting
Even with clear steps, things can go awry. Here are some common areas to check.
Secret Key Mismatches
- Typo: Triple-check that the
NEXTJS_PREVIEW_SECRETin your Next.js environment variables exactly matches the secret used in your WordPressfunctions.php. - Case Sensitivity: Secrets are usually case-sensitive.
- Environment Setup: Ensure your
NEXTJS_PREVIEW_SECRETis correctly set in your.env.localfor development and in your hosting platform’s environment variables for production.
Caching Issues
- Next.js Caching: Always use
cache: 'no-store'when fetching draft content, especially infetchcalls. - CDN/Edge Caching: If you’re using a CDN (Vercel Edge, Cloudflare, etc.), make sure your preview routes and APIs are configured not to cache requests that involve draft content or the preview API route. The
Cache-Control: no-storeheader should handle this correctly, but sometimes manual CDN rules are needed. - WordPress Object Caching: If you have object caching plugins (Redis, Memcached), ensure they don’t interfere with the live fetching of draft data.
Incorrect Redirects or Routing
- Next.js Route: Double-check that the
redirectPathin yourpages/api/preview.jscorrectly maps to your Next.js content page’s dynamic route (e.g.,/[postType]/[slug]). - WordPress Preview Link: Verify the
nextjs_base_urlin yourcustom_preview_linkfunction in WordPress.
WordPress API Access
- Permissions: If your custom REST endpoint or WPGraphQL queries aren’t returning drafts, it’s almost always a permissions issue. Ensure the
permission_callbackfor your custom endpoint is correct, or that your WPGraphQL authentication is granting sufficient privileges (edit_posts, etc.). - Availability: Is your WordPress API publicly accessible to your Next.js application server? Check firewalls or other restrictions.
Mixed Content
- If your Next.js site is HTTPS but your WordPress is HTTP, you might encounter mixed content warnings or blocked API requests in the browser console. Always use HTTPS for both.
Revisions vs. Drafts
- Be clear about whether you want to preview the current draft or a specific revision. The
asPreviewargument in WPGraphQL typically targets the latest draft/revision. For REST API, you might need to query revisions directly if you need older ones.
Debugging
- Browser Developer Tools: Watch the network requests when you click the WordPress preview button. Is it hitting your Next.js API route? What status codes are returned? What do the request and response bodies look like?
- Next.js Console Logs: Add
console.logstatements in your Next.js API routes andgetStaticProps/getServerSidePropsto see what data is being received and processed. - WordPress Debug Log: Enable
WP_DEBUG_LOGinwp-config.phpto catch any PHP errors on the WordPress side.
By systematically working through these points, you should be able to get your WordPress previews working seamlessly with your Next.js frontend, giving your content editors the smooth experience they deserve. Good luck!