How to Set Up Preview Mode
Enable draft preview mode to view unpublished content before it goes live.
Overview
Preview Mode allows content editors to preview draft content in your Next.js site before publishing. This guide shows how to set it up with Strapi-NextGen Framework.
Step 1: Create Preview Route Handler
Create app/api/preview/route.ts:
import { createPreviewHandler } from 'strapi-nextgen-framework';
// Create the handler with your secret
const handler = createPreviewHandler({
secret: process.env.PREVIEW_SECRET!,
});
export const GET = handler;Step 2: Add Environment Variable
Add to .env.local:
PREVIEW_SECRET=your-random-secret-here-minimum-32-charactersGenerate a secure secret:
openssl rand -base64 32Step 3: Create Exit Preview Route
Create app/api/exit-preview/route.ts:
import { createExitPreviewHandler } from 'strapi-nextgen-framework';
const handler = createExitPreviewHandler();
export const GET = handler;Step 4: Configure Strapi Preview Button
In your Strapi admin panel:
- Go to Settings → Preview
- Add a new preview configuration:
{
"url": "http://localhost:3000/api/preview",
"secret": "your-preview-secret"
}Step 5: Use Draft Mode in Pages
Update your page to fetch draft content:
import { draftMode } from 'next/headers';
import { strapi } from '@/lib/strapi';
import { GetArticleDocument } from '@/graphql/generated';
export default async function ArticlePage({
params
}: {
params: { slug: string }
}) {
// Check if draft mode is enabled
const { isEnabled } = draftMode();
// SDK automatically uses publicationState based on draft mode
const data = await strapi.getCollection('articles', GetArticleDocument, {
filters: { slug: { eq: params.slug } },
});
const article = data.articles?.data[0]?.attributes;
return (
<article>
{/* Show draft indicator */}
{isEnabled && (
<div className="bg-yellow-100 border-l-4 border-yellow-500 p-4 mb-4">
<p className="text-yellow-700">
🚧 Preview Mode - Viewing draft content
</p>
<a href="/api/exit-preview" className="underline">
Exit Preview
</a>
</div>
)}
<h1>{article?.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article?.content }} />
</article>
);
}How It Works
1. Preview URL
When you click "Preview" in Strapi, it redirects to:
http://localhost:3000/api/preview?secret=YOUR_SECRET&slug=/blog/my-article2. Handler Validation
The preview handler:
- ✅ Validates the secret
- ✅ Enables Next.js Draft Mode
- ✅ Redirects to the specified slug
- ❌ Returns 401 if secret invalid
3. Draft Content Fetching
The SDK automatically detects draft mode and:
- Sets
publicationState: PREVIEWin GraphQL queries - Fetches both published AND draft content
- Includes unpublished entries
4. Exit Preview
Clicking "Exit Preview" or visiting /api/exit-preview:
- ✅ Disables draft mode
- ✅ Redirects to homepage
- ✅ Restores normal (published-only) behavior
Advanced Configuration
Custom Redirect Logic
import { createPreviewHandler } from 'strapi-nextgen-framework';
const handler = createPreviewHandler({
secret: process.env.PREVIEW_SECRET!,
redirectOverride: (slug) => {
// Custom logic for preview URLs
if (slug?.startsWith('/blog/')) {
return `/preview${slug}`;
}
return slug || '/';
},
});
export const GET = handler;Preview Banner Component
Create a reusable preview banner:
import { draftMode } from 'next/headers';
export function PreviewBanner() {
const { isEnabled } = draftMode();
if (!isEnabled) return null;
return (
<div className="fixed top-0 left-0 right-0 bg-yellow-500 text-white p-2 text-center z-50">
<span className="font-semibold">Preview Mode Active</span>
{' - '}
<a
href="/api/exit-preview"
className="underline hover:no-underline"
>
Exit Preview
</a>
</div>
);
}Use in layout:
import { PreviewBanner } from '@/components/PreviewBanner';
export default function RootLayout({ children }) {
return (
<html>
<body>
<PreviewBanner />
{children}
</body>
</html>
);
}Multiple Content Types
Handle different content types in preview URL:
const handler = createPreviewHandler({
secret: process.env.PREVIEW_SECRET!,
redirectOverride: (slug) => {
const url = new URL(slug || '/', 'http://localhost:3000');
const contentType = url.searchParams.get('type');
const id = url.searchParams.get('id');
if (contentType === 'article') {
return `/blog/${id}`;
}
if (contentType === 'page') {
return `/${id}`;
}
return slug || '/';
},
});Security Best Practices
1. Use Strong Secrets
# Generate a secure 256-bit secret
openssl rand -base64 322. Environment Variables Only
Never commit secrets to git:
# ❌ DON'T
const secret = "my-secret-123";
# ✅ DO
const secret = process.env.PREVIEW_SECRET!;3. HTTPS in Production
Always use HTTPS for preview URLs in production:
https://your-site.com/api/preview?secret=...4. Restrict Access
Consider adding IP whitelisting or authentication for preview routes in production.
Troubleshooting
Preview Not Working
Problem: Clicking preview in Strapi doesn't show draft content
Solutions:
- Verify
PREVIEW_SECRETmatches in both .env.local and Strapi - Check secret is at least 32 characters
- Ensure preview route is at
/api/preview/route.ts - Verify draft mode is enabled:
const { isEnabled } = draftMode(); console.log('Draft mode:', isEnabled);
Wrong Content Showing
Problem: Still seeing published content in preview mode
Solutions:
- Check SDK is being called in server component
- Verify your GraphQL query doesn't override
publicationState - Clear Next.js cache:
rm -rf .next - Check Strapi has draft content
401 Unauthorized
Problem: Preview route returns 401
Solutions:
- Verify secret in URL matches
PREVIEW_SECRET - Check environment variables are loaded (restart dev server)
- Ensure secret has no trailing spaces/newlines
Exit Preview Not Working
Problem: Can't exit preview mode
Solutions:
- Check
/api/exit-preview/route.tsexists - Try manual visit:
http://localhost:3000/api/exit-preview - Clear cookies and restart browser
- Check for JavaScript errors in console
Testing Preview Mode
Manual Test
- Create draft content in Strapi
- Click "Preview" button
- Verify you're redirected to Next.js site
- Check draft content is visible
- Click "Exit Preview"
- Verify draft content is hidden
Automated Test
import { test, expect } from '@playwright/test';
test('preview mode shows draft content', async ({ page }) => {
// Enable preview mode
await page.goto('/api/preview?secret=test-secret&slug=/blog/draft-article');
// Verify preview banner
await expect(page.locator('text=Preview Mode Active')).toBeVisible();
// Verify draft content visible
await expect(page.locator('h1')).toContainText('Draft Article');
// Exit preview
await page.click('text=Exit Preview');
// Verify draft content hidden
await expect(page).toHaveURL('/');
});Next Steps
- Fetch Global Data - Add persistent headers/footers
- Set Up Revalidation - Auto-update on publish
- Error Handling - Handle missing draft content