Preview
With the Preview feature, you can preview your front end application directly from Strapi's admin panel. This is helpful to see how updates to your content in the Edit View of the Content Manager will affect the final result.
Live Preview available only with the CMS Growth and Enterprise plans.
config/admin
file

Configuration
-
The following environment variables must be defined in your
.env
file, replacing example values with appropriate values:CLIENT_URL=https://your-frontend-app.com
PREVIEW_SECRET=your-secret-keyThe
PREVIEW_SECRET
key is optional but required with Next.js draft mode. -
A front-end application for your Strapi project should be already created and set up.
Configuration components
The Preview feature configuration is stored in the preview
object of the config/admin
file and consists of 3 key components:
Activation flag
Enables or disables the preview feature:
// …
preview: {
enabled: true,
// …
}
// …
Allowed origins
Controls which domains can access previews:
// …
preview: {
enabled: true,
config: {
allowedOrigins: env("CLIENT_URL"), // Usually your frontend application URL
// …
}
}
// …
Preview handler
Manages the preview logic and URL generation, as in the following basic example where uid
is the content-type identifier (e.g., api::article.article
or plugin::my-api.my-content-type
):
// …
preview: {
enabled: true,
config: {
// …
async handler(uid, { documentId, locale, status }) {
const document = await strapi.documents(uid).findOne({ documentId });
const pathname = getPreviewPathname(uid, { locale, document });
return `${env('PREVIEW_URL')}${pathname}`
},
}
}
// …
An example of URL generation logic in given in the following basic implementation guide.
Previewing draft entries
The strategy for the front end application to query draft or published content is framework-specific. At least 3 strategies exist:
- using a query parameter, having something like
/your-path?preview=true
(this is, for instance, how Nuxt works) - redirecting to a dedicated preview route like
/preview?path=your-path
(this is, for instance, how Next's draft mode works) - or using a different domain for previews like
preview.mysite.com/your-path
.
When Draft & Publish is enabled for your content-type, you can also directly leverage Strapi's status
parameter to handle the logic within the Preview handler, using the following generic approach:
async handler(uid, { documentId, locale, status }) {
const document = await strapi.documents(uid).findOne({ documentId });
const pathname = getPreviewPathname(uid, { locale, document });
if (status === 'published') {
// return the published version
}
// return the draft version
},
A more detailed example using the draft mode of Next.js is given in the basic implementation guide.
Basic implementation guide
Follow these steps to add Preview capabilities to your content types.
1. [Strapi] Create the Preview configuration
Create a new file /config/admin.ts
(or update it if it exists) with the following basic structure:
export default ({ env }) => ({
// Other admin-related configurations go here
// (see docs.strapi.io/cms/configurations/admin-panel)
preview: {
enabled: true,
config: {
allowedOrigins: env('CLIENT_URL'),
async handler (uid, { documentId, locale, status }) => {
// Handler implementation coming in step 3
},
},
},
});
2. [Strapi] Add URL generation logic
Add the URL generation logic with a getPreviewPathname
function. The following example is taken from the Launchpad Strapi demo application:
// Function to generate preview pathname based on content type and document
const getPreviewPathname = (uid, { locale, document }): string => {
const { slug } = document;
// Handle different content types with their specific URL patterns
switch (uid) {
// Handle pages with predefined routes
case "api::page.page":
switch (slug) {
case "homepage":
return `/${locale}`; // Localized homepage
case "pricing":
return "/pricing"; // Pricing page
case "contact":
return "/contact"; // Contact page
case "faq":
return "/faq"; // FAQ page
}
// Handle product pages
case "api::product.product": {
if (!slug) {
return "/products"; // Products listing page
}
return `/products/${slug}`; // Individual product page
}
// Handle blog articles
case "api::article.article": {
if (!slug) {
return "/blog"; // Blog listing page
}
return `/blog/${slug}`; // Individual article page
}
default: {
return null;
}
}
};
// … main export (see step 3)
Some content types don't need to have a preview if it doesn't make sense, hence the default case returning null
. A Global single type with some site metadata, for example, will not have a matching front-end page. In these cases, the handler function should return null
, and the preview UI will not be shown in the admin panel. This is how you enable or disable preview per content type.
3. [Strapi] Add handler logic
Create the complete configuration, expanding the basic configuration created in step 1. with the URL generation logic created in step 2., adding an appropriate handler logic:
const getPreviewPathname = (uid, { locale, document }): string => {
// … as defined in step 2
};
// Main configuration export
export default ({ env }) => {
// Get environment variables
const clientUrl = env("CLIENT_URL"); // Frontend application URL
const previewSecret = env("PREVIEW_SECRET"); // Secret key for preview authentication
return {
// Other admin-related configurations go here
// (see docs.strapi.io/cms/configurations/admin-panel)
preview: {
enabled: true, // Enable preview functionality
config: {
allowedOrigins: clientUrl, // Restrict preview access to specific domain
async handler(uid, { documentId, locale, status }) {
// Fetch the complete document from Strapi
const document = await strapi.documents(uid).findOne({ documentId });
// Generate the preview pathname based on content type and document
const pathname = getPreviewPathname(uid, { locale, document });
// Disable preview if the pathname is not found
if (!pathname) {
return null;
}
// Use Next.js draft mode passing it a secret key and the content-type status
const urlSearchParams = new URLSearchParams({
url: pathname,
secret: previewSecret,
status,
});
return `${clientUrl}/api/preview?${urlSearchParams}`;
},
},
},
};
};
4. [Front end] Set up the front-end preview route
Setting up the front-end preview route is highly dependent on the framework used for your front-end application.
For instance, Next.js draft mode and Nuxt preview mode provide additional documentation on how to implement the front-end part in their respective documentations.
If using Next.js, a basic implementation could be like in the following example taken from the Launchpad Strapi demo application:
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
export async function GET(request: Request) {
// Parse query string parameters
const { searchParams } = new URL(request.url);
const secret = searchParams.get("secret");
const url = searchParams.get("url");
const status = searchParams.get("status");
// Check the secret and next parameters
// This secret should only be known to this route handler and the CMS
if (secret !== process.env.PREVIEW_SECRET) {
return new Response("Invalid token", { status: 401 });
}
// Enable Draft Mode by setting the cookie
if (status === "published") {
draftMode().disable();
} else {
draftMode().enable();
}
// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
redirect(url || "/");
}
5. [Front end] Allow the front-end to be embedded
On the Strapi side, the allowedOrigins
configuration parameter allows the admin panel to load the front-end window in an iframe. But allowing the embedding works both ways, so on the front-end side, you also need to allow the window to be embedded in Strapi's admin panel.
This requires the front-end application to have its own header directive, the CSP frame-ancestors
directive. Setting this directive up depends on how your website is built. For instance, setting this up in Next.js requires a middleware configuration (see Next.js docs).
6. [Front end] Adapt data fetching for draft content
Once the preview system is set up, you need to adapt your data fetching logic to handle draft content appropriately. This involves the following steps:
- Create or adapt your data fetching utility to check if draft mode is enabled
- Update your API calls to include the draft status parameter when appropriate
The following, taken from the Launchpad Strapi demo application, is an example of how to implement draft-aware data fetching in your Next.js front-end application:
import { draftMode } from "next/headers";
import qs from "qs";
export default async function fetchContentType(
contentType: string,
params: Record = {}
): Promise {
// Check if Next.js draft mode is enabled
const { isEnabled: isDraftMode } = await draftMode();
try {
const queryParams = { ...params };
// Add status=draft parameter when draft mode is enabled
if (isDraftMode) {
queryParams.status = "draft";
}
const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch data from Strapi (url=${url}, status=${response.status})`
);
}
return await response.json();
} catch (error) {
console.error("Error fetching content:", error);
throw error;
}
}
This utility method can then be used in your page components to fetch either draft or published content based on the preview state:
// In your page component:
const pageData = await fetchContentType('api::page.page', {
// Your other query parameters
});
Live Preview implementation GrowthThis feature is available with a Growth plan. EnterpriseThis feature is available with an Enterprise plan.
After setting up the basic Preview feature, you can enhance the experience by implementing Live Preview.
Window messages
Live Preview creates a more interactive experience by communicating between the admin and your frontend. It relies on events posted through the postMessage()
API on the window
object.
You need to add an event listener in your application. It should be present on all pages, ideally in a layout component that wraps your entire application. The listener needs to filter through messages and react only to Strapi-initiated messages.
There are 2 messages to listen to:
strapiUpdate
: sent by Strapi when a content update has been saved to the database. It's an opportunity to fetch the updated version of the content and refresh the preview. With Next.js, the recommended way to refresh the iframe content is with the `router.refresh()` method.previewScript
: sent by Strapi to give you a script that powers the Live Preview functionality. This script should be injected into the page's<head>
tag. It handles highlighting editable areas in the preview and sending messages back to Strapi when an area is double-clicked for editing.
In order to receive the previewScript
message, you need to let Strapi know that your frontend is ready to receive it. This is done by posting a previewReady
message to the parent window.
When putting it all together, a component ready to be added to your global layout could look like:
- JavaScript
- TypeScript
'use client';
export default function LivePreview() {
// …
const router = useRouter();
useEffect(() => {
const handleMessage = async (message) => {
const { origin, data } = message;
if (origin !== process.env.NEXT_PUBLIC_API_URL) {
return;
}
if (data.type === 'strapiUpdate') {
router.refresh();
} else if (data.type === 'strapiScript') {
const script = window.document.createElement('script');
script.textContent = data.payload.script;
window.document.head.appendChild(script);
}
};
// Add the event listener
window.addEventListener('message', handleMessage);
// Let Strapi know we're ready to receive the script
window.parent?.postMessage({ type: 'previewReady' }, '*');
// Remove the event listener on unmount
return () => {
window.removeEventListener('message', handleMessage);
};
}, [router]);
return null;
}
'use client';
export default function LivePreview() {
// …
const router = useRouter();
useEffect(() => {
const handleMessage = async (message: MessageEvent<any>) => {
const { origin, data } = message;
if (origin !== process.env.NEXT_PUBLIC_API_URL) {
return;
}
if (data.type === 'strapiUpdate') {
router.refresh();
} else if (data.type === 'strapiScript') {
const script = window.document.createElement('script');
script.textContent = data.payload.script;
window.document.head.appendChild(script);
}
};
// Add the event listener
window.addEventListener('message', handleMessage);
// Let Strapi know we're ready to receive the script
window.parent?.postMessage({ type: 'previewReady' }, '*');
// Remove the event listener on unmount
return () => {
window.removeEventListener('message', handleMessage);
};
}, [router]);
return null;
}
Caching in Next.js:
In Next.js, cache persistence may require additional steps. You might need to invalidate the cache by making an API call from the client side to the server, where the revalidation logic will be handled. Please refer to Next.js documentation for details, for instance with the revalidatePath() method.
Content source maps
Live Preview is able to identify the parts of your frontend that correspond to fields in Strapi. This is done through content source maps, which are metadata encoded as hidden characters in your string-based content (e.g., text fields). It uses the @vercel/stega library to encode and decode this metadata.
Metadatas will only be added in your Content API responses when the strapi-encode-source-maps
header is set to true
. You can set this header in your data fetching utility. Make sure to only pass the header when you detect that your site is rendered in a preview context.
For a Next.js application, you may use the draftMode()
method from next/headers
to detect if draft mode is enabled, and set the header accordingly in all your API calls:
import { draftMode } from "next/headers";
import qs from "qs";
export default async function fetchContentType(
contentType: string,
params: Record = {}
): Promise {
// Check if Next.js draft mode is enabled
const { isEnabled: isDraftMode } = await draftMode();
try {
const queryParams = { ...params };
// Add status=draft parameter when draft mode is enabled
if (isDraftMode) {
queryParams.status = "draft";
}
const url = `${baseURL}/${contentType}?${qs.stringify(queryParams)}`;
const response = await fetch(url, {
headers: {
// Enable content source maps in preview mode
"strapi-encode-source-maps": isDraftMode ? "true" : "false",
},
});
if (!response.ok) {
throw new Error(
`Failed to fetch data from Strapi (url=${url}, status=${response.status})`
);
}
return await response.json();
} catch (error) {
console.error("Error fetching content:", error);
throw error;
}
}
Usage
Path to use the feature: Content Manager, edit view of your content type
Based on your CMS plan, your experience with Preview will be different:
- With the Free plan, Preview will be full screen only.
- With the GrowthThis feature is available with a Growth plan. and EnterpriseThis feature is available with an Enterprise plan. plans, you get access to an enhanced experience called Live Preview. With Live Preview, you can see the Preview alongside the Edit view of the Content Manager, and you can also edit the content directly within the preview itself by double-clicking on any content.
Once the Preview feature is properly set up, an Open preview button is visible on the right side of the Content Manager's edit view. Clicking it will display the preview of your content as it will appear in your front-end application, but directly within Strapi's the admin panel.


Once the Preview is open, you can:
- click the close button in the upper left corner to go back to the Edit View of the Content Manager,
- switch between the Desktop and Mobile preview using the dropdown above the previewed content,
- switch between previewing the draft and the published version (if Draft & Publish is enabled for the content-type),
- and click the link icon in the upper right corner to copy the preview link. Depending on the preview tab you are currently viewing, this will either copy the link to the preview of the draft or the published version.
In the Edit view of the Content Manager, the Open preview button will be disabled if you have unsaved changes. Save your latest changes and you should be able to preview content again.
Live Preview
GrowthThis feature is available with a Growth plan. EnterpriseThis feature is available with an Enterprise plan.Live Preview is the enhanced Preview experience available with Strapi’s paid CMS plans.
With Live Preview, in addition to what’s included in the Free plan, you can:
- Use the Side Editor to view both the entry’s Edit view in the Content Manager and the front-end preview side by side. You can also switch between full-screen and side-by-side preview using the and buttons.
- Double-click any content in the preview pane to edit it in place. This opens a popover that syncs the front-end content with the corresponding field in Strapi.


This feature is currently experimental. Feel free to share feedback or issues with the Strapi team.
The current version of Live Preview comes with the following limitations:
- Blocks fields are not detected, and changing them in the Side Editor won’t be reflected in the preview. Clicking on Save after updates should however still work.
- Media assets and fields in dynamic zones are not handled.