Strapi 5 introduces a powerful preview feature that lets content editors preview draft content directly on the frontend before publishing.
In this guide, we’ll walk through how to set up the preview feature from scratch using a starter project with Strapi 5 and Next.js 15. We’ll cover both backend and frontend setup so you can enable your content team to view draft content with just one click.
note: Although this guide is for Next.js 15, the same principles apply to other frameworks like React, Nuxt, etc.
Why Use the Preview Feature?
The preview feature lets you view draft content on your site without publishing it.
This is perfect for editorial workflows where content needs review or approval before going live.
Editors can see exactly how their content will look once published, helping catch layout or formatting issues early.
Try the Feature in the Strapi Demo
Before setting things up locally, you can also try it out via Strapi’s live demo.
Which includes additional feature that allows you to edit content on fly in our preview side editor which is available in our Growth Plan or Enterprise Edition.
Just click “Start your demo”, fill out your info, and once provisioned, you’ll have access to both the Strapi backend and the Next.js frontend.
In the admin panel, go to Content Manager > Articles, click an article, and you’ll see a Preview button. Clicking it opens a draft view of the article as it would appear on the live site.
But for this guide we will be using the Community Edition. Which is free to use and gives you the ability to preview your content on the frontend before publishing it both in the preview and live mode.
Resources:
If you prefer to watch a video, here is the video version of this guide.
Project Setup
To save some time we will be using the starter project that I created for this guide.
It is a a simple blog starter project that includes a few articles and a few categories with a Next.js frontend.
To get started locally:
Step 1: Clone the starter project from GitHub
Open your favorite terminal and run the following command to clone the starter project from GitHub.
git clone https://github.com/PaulBratslavsky/strapi-next-js-preview-starter-code
Step 2: Install dependencies
Navigate to the project directory and install the dependencies with the following command.
cd strapi-next-js-preview-starter-code
npm run setup
This will install the dependencies and configure the environment for both the frontend and the backend.
Step 3: Seed the database
Run the following command to seed the database with the demo data.
npm run seed
This will populate the database with the demo data.
> strapi-nextjs-preview-example@1.0.0 seed
> cd server && yarn strapi import -f ../seed-data.tar.gz --force
yarn run v1.22.22
warning ../package.json: No license field
$ strapi import -f ../seed-data.tar.gz --force
? The import will delete your existing data! Are you sure you want to proceed? Yes
--------------------------------
In article-populate middleware.
--------------------------------
--------------------------------
In article-populate middleware.
--------------------------------
Starting import...
✔ entities: 50 transferred (size: 46.2 KB) (elapsed: 51 ms) (906.8 KB/s)
✔ assets: 52 transferred (size: 7.4 MB) (elapsed: 620 ms) (12 MB/s)
✔ links: 132 transferred (size: 25.3 KB) (elapsed: 6 ms) (4.1 MB/s)
✔ configuration: 37 transferred (size: 115.2 KB) (elapsed: 5 ms) (22.5 MB/s)
┌─────────────────────────────────────────┬───────┬───────────────┐
│ Type │ Count │ Size │
├─────────────────────────────────────────┼───────┼───────────────┤
│ entities │ 50 │ 46.2 KB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::about.about │ 1 │ ( 689 B ) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::article.article │ 10 │ ( 20.9 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::author.author │ 2 │ ( 536 B ) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::category.category │ 5 │ ( 1.3 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- api::global.global │ 1 │ ( 372 B ) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- plugin::i18n.locale │ 1 │ ( 253 B ) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- plugin::upload.file │ 11 │ ( 16.8 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- plugin::users-permissions.permission │ 17 │ ( 4.7 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- plugin::users-permissions.role │ 2 │ ( 656 B ) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ assets │ 52 │ 7.4 MB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- .jpeg │ 44 │ ( 6.8 MB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ -- .png │ 8 │ ( 690.1 KB) │
├─────────────────────────────────────────┼───────┼───────────────┤
│ links │ 132 │ 25.3 KB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ configuration │ 37 │ 115.2 KB │
├─────────────────────────────────────────┼───────┼───────────────┤
│ Total │ 271 │ 7.6 MB │
└─────────────────────────────────────────┴───────┴───────────────┘
Import process has been completed successfully!
✨ Done in 3.09s.
Step 4: Start the project
Start the project with the following command.
npm run dev
The frontend runs on http://localhost:3000, and the Strapi backend on http://localhost:1337
Let's navigate to http://localhost:1337
you will be greeted with the following screen. Go ahead and create your first Strapi Admin user.
Once created you will be redirected to the Strapi Admin dashboard.
If we navigate to http://localhost:3000
you will see the following screen.
Nice, now we are ready to start building our preview feature.
How To Setup The Preview Feature In Strapi and Next.js
Before we start, in our Strapi Admin, let's navigate to one of our articles and you will notice that their is no Preview button in sight.
That is because we have not configured the preview feature in Strapi yet. Let's do that now.
Configuring the Preview Feature in Strapi
Inside of our Strapi project folder named server, go to config/admin.ts
and paste in the preview configuration and paste in the following code after all the other configurations.
1preview: {
2 enabled: true,
3 config: {
4 allowedOrigins: [env("CLIENT_URL")],
5 async handler(uid, { documentId, locale, status }) {
6 const document = await strapi.documents(uid).findOne({
7 documentId,
8 populate: null,
9 fields: ["slug"],
10 });
11 const { slug } = document;
12
13 const urlSearchParams = new URLSearchParams({
14 secret: env("PREVIEW_SECRET"),
15 ...(slug && { slug }),
16 uid,
17 status,
18 });
19
20 const previewURL = `${env("CLIENT_URL")}/api/preview?${urlSearchParams}`;
21 return previewURL;
22 },
23 },
24 },
enabled: Turns on the preview feature in Strapi.
config.allowedOrigins: Specifies which frontend domains can receive preview links. Uses CLIENT_URL from environment variables.
config.handler(uid, { documentId, locale, status }): Async function that:
- Fetches the document by documentId using strapi.documents(uid).findOne(...).
- Extracts the slug from the document.
- Constructs a preview URL with query params including slug, uid, status, and a PREVIEW_SECRET.
- Returns the preview URL to be used by the frontend (e.g. Next.js preview route).
Add finally let's add the following environment variables in .env
:
CLIENT_URL=http://localhost:3000
PREVIEW_SECRET=your-secret-key
If we navigate back to our article in the Strapi Admin, you will notice that the Preview button is now visible.
If we click on the Preview button, we will see the following screen.
Notice it is not working yet. That is because we have not setup the preview feature in the Next.js frontend. Let's do that now.
How To Setup The Preview Feature In Next.js
In order to receive the preview URL from Strapi, we need to create a preview route in our Next.js app.
Let's create a folder called app/api/preview
and inside it create a file called route.ts
.
1// app/api/preview/route.ts
2
3import { draftMode } from "next/headers";
4import { redirect } from "next/navigation";
5
6function getPreviewPath(
7 contentType: string | undefined,
8 slug: string | null,
9 locale: string | null,
10 status: string | null
11): string {
12 const basePath = (() => {
13 if (!contentType) return "/";
14
15 if (contentType === "article" || contentType.includes("articles")) {
16 return slug ? "/articles/" + slug : "/articles";
17 }
18
19 // Can add other content types here
20
21 // if (contentType === 'page' || contentType.includes('pages')) {
22 // return slug ? '/' + slug : '/';
23 // }
24
25 return "/" + contentType;
26 })();
27
28 const localePath =
29 locale && locale !== "en" ? "/" + locale + basePath : basePath;
30 const statusParam = status ? "?status=" + status : "";
31 return localePath + statusParam;
32}
33
34export const GET = async (request: Request) => {
35 // Parse query string parameters
36 const { searchParams } = new URL(request.url);
37 const searchParamsData = Object.fromEntries(searchParams);
38 const { secret, slug, locale, uid, status } = searchParamsData;
39
40 console.log(searchParamsData);
41
42 // Check the secret and next parameters
43 if (secret !== process.env.PREVIEW_SECRET) {
44 return new Response("Invalid token", { status: 401 });
45 }
46
47 const contentType = uid?.split(".").pop();
48 const finalPath = getPreviewPath(contentType, slug, locale, status);
49
50 // Enable Draft Mode by setting the cookie
51 const draft = await draftMode();
52 status === "draft" ? draft.enable() : draft.disable();
53
54 // Redirect to the path from the fetched post
55 redirect(finalPath);
56};
Understanding the route.ts Preview Handler in Next.js
To enable content previews in a Next.js site powered by a headless CMS like Strapi, you need a backend route that handles preview requests securely.
Below is a breakdown of how the route.ts file works to enable Draft Mode and redirect users to a previewable version of the content.
Step 1: Parsing Query Parameters The handler reads the preview request parameters from the URL:
1const { secret, slug, locale, uid, status } = Object.fromEntries(
2 new URL(request.url).searchParams
3);
These parameters are typically passed from the CMS when a user clicks “Preview” on a content entry.
Step 2: Validating the Secret Token To ensure the request is authorized, the code checks for a secret query param and compares it to an environment variable:
1if (secret !== process.env.PREVIEW_SECRET) {
2 return new Response("Invalid token", { status: 401 });
3}
If the secret doesn't match, the request is rejected.
Step 3: Determining the Content Type The code extracts the content type (e.g. post, page) from the UID:
1const contentType = uid?.split(".").pop();
This helps define the preview path later.
Step 4: Generating the Preview Path
A helper function, getPreviewPath
, constructs the preview URL based on:
- Content type (e.g. post or page)
- Slug (e.g. /blog/my-article)
- Locale (e.g. /fr/blog/mon-article)
- Status (e.g. ?status=draft)
Example:
1"/articles/my-article?status=draft";
Step 5: Enabling Draft Mode Next.js’s Draft Mode is toggled based on the status value:
1status === "draft" ? draft.enable() : draft.disable();
When enabled, it allows the frontend to fetch and display unpublished content.
Step 6: Redirecting to the Preview URL Finally, the user is redirected to the preview URL:
1redirect(finalPath);
This takes them to the correct page with Draft Mode enabled if needed.
This preview handler:
- Secures access with a shared secret
- Parses incoming request data
- Enables or disables Next.js Draft Mode
- Redirects users to the correct preview path
Now that we have our preview route setup, we just need to modify our code in the client/app/articles/[slug]/page.tsx
file to use the Draft Mode.
Let's import the draftMode
function from the next/headers
module:
1import { draftMode } from "next/headers";
We will use the draftMode
function to check if the page is in Draft Mode and if it is, we will query the API based on the status.
Let's add the following code to the client/app/articles/[slug]/page.tsx
file:
1const { isEnabled: isDraftMode } = await draftMode();
2const status = isDraftMode ? "draft" : "published";
3const { data } = await getArticleBySlug(slug, status)
4if (data.length === 0) notFound();
5const article = data[0]
6
7return (
8 ...
9)
The completed code will look like this:
1import Link from "next/link";
2import { notFound } from "next/navigation";
3import { formatDate } from "@/lib/utils";
4import BlockRenderer from "@/components/block-renderer";
5import { ArrowLeft } from "lucide-react";
6import { getArticleBySlug } from "@/lib/strapi-client";
7import { StrapiImage } from "@/components/custom/strapi-image";
8import { draftMode } from "next/headers";
9
10export default async function ArticlePage({
11 params,
12}: {
13 params: Promise<{ slug: string }>;
14}) {
15 const resolvedParams = await params;
16 const slug = resolvedParams.slug;
17
18 const { isEnabled: isDraftMode } = await draftMode();
19 const status = isDraftMode ? "draft" : "published";
20 const { data } = await getArticleBySlug(slug, status);
21 if (data.length === 0) notFound();
22 const article = data[0];
23
24 return (
25 <main className="container mx-auto px-4 py-8 max-w-4xl">
26 <Link
27 href="/"
28 className="inline-flex items-center text-blue-600 hover:text-blue-800 mb-6"
29 >
30 <ArrowLeft className="w-4 h-4 mr-2" />
31 Back to articles
32 </Link>
33
34 <article>
35 <div className="mb-8">
36 <h1 className="text-4xl font-bold mb-4">{article.title}</h1>
37 <div className="flex items-center mb-6">
38 <div className="relative h-10 w-10 rounded-full overflow-hidden mr-3">
39 <StrapiImage
40 src={
41 article.author.avatar.url ||
42 "/placeholder.svg?height=50&width=50&query=avatar"
43 }
44 alt={article.author.name}
45 fill
46 className="object-cover"
47 />
48 </div>
49 <div>
50 <p className="font-medium">{article.author.name}</p>
51 <p className="text-sm text-gray-600">
52 Published on {formatDate(article.publishedAt)}
53 </p>
54 </div>
55 <span className="mx-3 text-gray-300">|</span>
56 <span className="bg-gray-100 rounded-full px-3 py-1 text-sm text-gray-700">
57 {article.category.name}
58 </span>
59 </div>
60 </div>
61
62 <div className="relative w-full h-96 mb-8 rounded-lg overflow-hidden">
63 <StrapiImage
64 src={
65 article.cover.url ||
66 "/placeholder.svg?height=600&width=1200&query=article cover"
67 }
68 alt={article.cover.alternativeText || article.title}
69 fill
70 className="object-cover"
71 priority
72 />
73 </div>
74
75 <div className="prose prose-lg max-w-none">
76 {article.blocks.map((block: any, index: number) => (
77 <BlockRenderer key={index} block={block} />
78 ))}
79 </div>
80 </article>
81 </main>
82 );
83}
We need to update the getArticleBySlug
function to accept the status parameter.
Let's make the changes to the getArticleBySlug
function in the client/lib/strapi-client.ts
file:
1export async function getArticleBySlug(slug: string, status: string) {
2 console.log(slug, "slug");
3 const article = await client.collection("articles").find({
4 filters: {
5 slug: { $eq: slug },
6 },
7 status: status as "draft" | "published" | undefined,
8 });
9 return article;
10}
Finally, let's update the env
variables in the .env
in our next.js project with the following values.
STRAPI_BASE_URL=http://localhost:1337
PREVIEW_SECRET=your-secret-key
Note: The PREVIEW_SECRET
should be the same as the one in our Strapi project.
Now that everything is setup, we can start the Strapi server and the Next.js server by running the following command in the root of the project.
npm run dev
Now, back in our Strapi Admin, let's navigate to one of our articles and click the Preview button.
Nice, we are now able to preview our content in the Next.js frontend.
Summary
Strapi 5’s preview feature lets content editors view draft content directly on the frontend before it’s published—perfect for editorial review workflows.
In this guide, we used a starter project to walk through:
- Setting up Strapi with preview enabled using `config/admin.ts
- Creating a secure preview route in Next.js with
app/api/preview/route.ts
- Toggling draft mode and redirecting to the correct content path
- Updating the article page to support previewing draft vs published content
- Passing the correct status (draft or published) to the
getArticleBySlug
function
Conclusion
With just a few configuration changes, you can unlock a powerful preview experience for your content team using Strapi and Next.js.
This workflow allows editors to catch mistakes, improve layout, and confidently review changes before going live—all with the click of a Preview button.
For more advanced customizations, see the following talks from StrapiConf by Rémi de Juvigny.
Happy coding!
Paul
P.S: Come and hang out with me at Strapi's Discord
We have open office hours every week Monday through Friday at 12:30pm CST.
Come and hang out with us, ask questions, or just say hi!