Introduction to Strapi 5 Preview Feature
The Strapi Preview feature allows you to preview your frontend application directly from Strapi's admin panel. This makes content management a whole lot smarter.
If you've been struggling to provide a preview experience for your team, you're in for a treat. The new Strapi 5 release brings the preview functionality that changes how you manage and visualize content across multiple platforms.
Let's dive into the nuts and bolts of this powerful feature and explore how it can transform your content management process.
Video Resource
The complete video for this tutorial is available here
GitHub Repo for Complete Code
The complete code for this tutorial can be found in this project repo.
Also, you can clone the strapiconf
branch of the LaunchPad repo to get the complete code.
Pre-requisites
For this tutorial, ensure you have Strapi LaunchPad installed and running on your local machine.
See this tutorial on how to set up LaunchPad locally.
How Strapi Preview Feature Works (Behind the Scenes)
The Strapi preview is a single iframe that puts your frontend inside Strapi. An HTML iframe is used to display a web page within a web page.
To set up a preview in Strapi, we first need to know the URL to render on the iframe. To get this URL, we need to provide a function in the Strapi config file that takes the entry the user is looking for. So when a user is looking at the Article content type, they get to see a Blog URL. See the image below.
In the image above, the preview URL is the result of the function of the entry you provide. So if you are viewing the Article content type, the preview URL should be a Blog URL.
Similar to the image above, the logic function for a basic preview is usually in this form:
1// Path: ./config/admin.js
2
3switch (model) {
4 case "api::page.page":
5 if (slug === "homepage") {
6 return "/";
7 }
8 return `/${slug}`;
9 case "api::article.article":
10 return `/blog/${slug}`;
11 default:
12 return null;
13}
Strapi Preview Architecture
The Strapi Preview feature architecture looks like this.
In the image above, which shows the Strapi preview architecture, the blue box represents Strapi which renders the iframe in green. The config is what provides the URL or source of the iframe.
So far, the basic preview setup is great, however, there is a problem. The problem is that it is limited! What happens when you want to preview some content or draft that is not live yet?
The solution will be that you need to be able to preview it directly on your Strapi admin without affecting the live production version that your clients are going to use. For this reason, we need to set up the preview mode in the config.
Let's set up preview mode to allow you to preview drafts that are not live yet without affecting your Strapi production version.
How to Set Up Preview Mode in Strapi Using LaunchPad
In order to continue with setting up the preview mode and other sections of this tutorial we will be using LaunchPad, the official Strapi demo app.
👋 NOTE: You can learn how to set up LaunchPad locally and also deploy to Strapi cloud using this blog post.
Navigate to the config file ./config/admin.js
and update the code inside with the following code:
1// Path: ./config/admin.js
2
3const getPreviewPathname = (model, { locale, document }): string | null => {
4 const { slug } = document;
5 const prefix = `/${locale ?? "en"}`;
6
7 switch (model) {
8 case "api::page.page":
9 if (slug === "homepage") {
10 return prefix;
11 }
12 return `${prefix}/${slug}`;
13 case "api::article.article":
14 return `${prefix}/blog/${slug}`;
15 case "api::product.product":
16 return `${prefix}/products/${slug}`;
17 case "api::product-page.product-page":
18 return `${prefix}/products`;
19 case "api::blog-page.blog-page":
20 return `${prefix}/blog`;
21 default:
22 return null;
23 }
24};
25
26export default ({ env }) => {
27 const clientUrl = env("CLIENT_URL");
28
29 return {
30 auth: {
31 secret: env("ADMIN_JWT_SECRET"),
32 },
33 apiToken: {
34 salt: env("API_TOKEN_SALT"),
35 },
36 transfer: {
37 token: {
38 salt: env("TRANSFER_TOKEN_SALT"),
39 },
40 },
41 flags: {
42 nps: env.bool("FLAG_NPS", true),
43 promoteEE: env.bool("FLAG_PROMOTE_EE", true),
44 },
45 preview: {
46 enabled: true,
47 config: {
48 allowedOrigins: [clientUrl, "'self'"],
49 async handler(model, { documentId, locale, status }) {
50 const document = await strapi.documents(model).findOne({
51 documentId,
52 fields: ["slug"],
53 });
54
55 const pathname = getPreviewPathname(model, { locale, document });
56
57 // Disable preview if the pathname is not found
58 if (!pathname) {
59 return null;
60 }
61
62 const urlSearchParams = new URLSearchParams({
63 secret: env("PREVIEW_SECRET"),
64 pathname,
65 status,
66 documentId,
67 clientUrl,
68 });
69
70 return `${clientUrl}/api/preview?${urlSearchParams}`;
71 },
72 },
73 },
74 };
75};
Here is what we did above:
- We created a logic to generate the preview URL.
- The
getPreviewPathname
function holds to the switch logic we mentioned earlier that computes the URL of the preview. So this gives us thepathname
. - Instead of directly returning the pathname, we want to put it inside a search params object
urlSearchParams
object. - The URL we are redirecting to or returning is an API endpoint:
${clientUrl}/api/preview?${urlSearchParams}
that is a Next.js application. - Because we provided a secret
clientSecret
as a search parameter, the Next.js application will perform authentication to make sure that it is a Strapi admin user making the request. - The Next.js app will also check for the
status
to see if it is a draft which sets a cookie that says Strapi in draft mode. It will then redirect to the actualpathname
, read the cookies to see that it is in draft mode, and adjust the content it is going to fetch.
The code above is nothing new. This is how Preview has worked for quite some time. In summary, instead of returning the actual preview frontend, we are adding an API endpoint in between.
However, what happens when your company grows and you have many websites that want to consume the same content or even a native mobile application? In this case, it won't be one entry equals one URL.
This shows that our setup has its limit. Now let's create another layer for multi-frontend preview in Strapi.
Strapi Multi-Frontend Preview Setup - Web + Native
If you want to consume the same content across multiple websites or native applications you will need a multi-frontend preview setup. This is a proxy preview concept. Take a look at the image below:
In the image above, Strapi represents the blue box. Instead of directly rendering your Next.js apps or your native apps which represent the orange boxes, you can have a single frontend that is dedicated to hosting the preview.
This single frontend is known as the proxy, which is represented by the green box above. The proxy will render multiple types of previews. The types of previews could be simultaneous previews, setting up tabs, or any kind of preview depending on your choice.
Let's set up a multi-frontend proxy page that will allow rendering multiple types of previews.
Creating a Custom Proxy Page Inside Strapi.
The proxy we will create will act as an intermediary, allowing you to switch between different preview types.
Since we are already in the Strapi dashboard, we can use the Strapi API to create routes in Strapi instead of creating an external React application or another frontend which might make the setup a complex task.
Step 1: Create a New URL
Head over to the config file we updated recently and modify the redirecting client URL with the following code:
1// Path: ./config/admin.ts
2
3...
4
5return `/admin/preview-proxy?${urlSearchParams}`;
6
7...
In the code above the /admin
route is because we are working inside Strapi admin, and the nonexistent route is preview-proxy
.
When you click the "Open preview" button, you will see that we now embedded Strapi admin within itself.
Step 2: Create Custom Admin Route for Proxy Preview
Let's create the preview-proxy
non-existent route. The way to create an admin route inside of Strapi is to head over to ./src/admin
create a file called app.tsx
and add the following code.
1// Path: ./src/admin/app.tsx
2
3import type { StrapiApp } from "@strapi/strapi/admin";
4import { lazy } from "react";
5
6const PreviewProxy = lazy(() => import("./PreviewProxy"));
7
8export default {
9 config: {
10 locales: ["en"],
11 },
12 register(app: StrapiApp) {
13 app.router.addRoute({
14 path: "preview-proxy",
15 element: <PreviewProxy />,
16 });
17 },
18};
With the new router API app.router
, we created a route preview-proxy
which renders the PreviewProxy React component. Let's create the PreviewProxy component.
Step 3: Create a Default Component for Preview
Head over to ./strapi/src/admin/
and add the following code. The code below will render a box for the preview.
1// Path: LaunchPad/strapi/src/admin/PreviewProxy.tsx
2
3// ... imports
4
5const PreviewProxy = () => {
6 // ... app variables
7
8 return (
9 <Portal>
10 <Box
11 position="fixed"
12 top="0"
13 left="0"
14 right="0"
15 bottom="0"
16 background="neutral100"
17 zIndex={4}
18 ></Box>
19 </Portal>
20 );
21};
22
23export default PreviewProxy;
The code above uses the Strapi Design system. position=fixed
is used because we don't want to have two navigation menus within the app. You can choose to change the background to any color of your choice. For this tutorial, we will use neutral100
.
Recall that we want to be able to render for the web and native app. So, let's build this using Select.
Adding Support for Web and Mobile App Preview
Now that we have a proxy page for multiple-frontend, let's add a select logic to switch between web and native mobile apps.
We will be using Expo for our React Native application.
Add a device selector logic in the ./src/admin/PreviewProxy.tsx
file:
1// Path: LaunchPad/strapi/src/admin/PreviewProxy.tsx
2
3// ... imports
4
5const PreviewProxy = () => {
6 // ... app variables
7
8 return (
9 <Portal>
10 <Box
11 // ... Box styling
12 >
13 // Selector Logic
14 <Flex gap={4} justifyContent="center" padding={2}>
15 <Typography>Preview on:</Typography>
16 <SingleSelect value={selectedDevice.id} onChange={handleDeviceChange}>
17 {devices.map((device) => (
18 <SingleSelectOption key={device.id} value={device.id}>
19 {device.name}
20 </SingleSelectOption>
21 ))}
22 </SingleSelect>
23 </Flex>
24 </Box>
25 </Portal>
26 );
27};
28
29export default PreviewProxy;
This is what you should see.
We can now switch between web and mobile apps.
Step 1: Rendering the Different Previews
The next goal is to render the actual preview below the selection.
We will toggle between web and mobile.
This is the code for it.
1// Path: LaunchPad/strapi/src/admin/PreviewProxy.tsx
2
3// ... imports
4
5const PreviewProxy = () => {
6 // ... app variables
7
8 return (
9 <Portal>
10 <Box
11 // ... Box styling
12 >
13 // Selector Logic
14 <Flex gap={4} justifyContent="center" padding={2}>
15 <Typography>Preview on:</Typography>
16 <SingleSelect value={selectedDevice.id} onChange={handleDeviceChange}>
17 {devices.map((device) => (
18 <SingleSelectOption key={device.id} value={device.id}>
19 {device.name}
20 </SingleSelectOption>
21 ))}
22 </SingleSelect>
23 </Flex>
24
25 // Toggle Between Expo React Native and Web
26 {isMobileApp ? (
27 <ExpoPreview />
28 ) : (
29 <Box
30 tag="iframe"
31 src={previewURL}
32 width={selectedDevice.width}
33 height={selectedDevice.height}
34 />
35 )}
36 </Box>
37 </Portal>
38 );
39};
40
41export default PreviewProxy;
We can now toggle between the web and the native app. For the web devices, we computed the height and width programmatically.
Let's create the ExpoPreview component.
Step 2: Create ExpoPreview Component
Create a component for previewing the Expo QR code.
1// Path: LaunchPad/strapi/src/admin/utils/ExpoPreview.tsx
2
3import * as React from "react";
4import { Flex } from "@strapi/design-system";
5
6export const ExpoPreview = () => {
7 const qrCodeSrc = React.useMemo(() => {
8 const qrCodeUrl = new URL("https://qr.expo.dev/eas-update");
9 qrCodeUrl.searchParams.append(
10 "projectId",
11 "4327bdd6-9794-49d7-9b95-6a5198afd339",
12 );
13 qrCodeUrl.searchParams.append("runtimeVersion", "1.0.0");
14 qrCodeUrl.searchParams.append("channel", "default");
15 return qrCodeUrl.toString();
16 }, []);
17
18 return (
19 <Flex
20 display="flex"
21 alignItems="center"
22 justifyContent="center"
23 height="100%"
24 width="100%"
25 >
26 <img src={qrCodeSrc} alt="Expo QR Code" />
27 </Flex>
28 );
29};
The ExpoPreview component above displays a QR code for you to scan and use the EAS service to preview on your device.
Step 3: Configure Expo Image Content Security Policy
You will need to create a security configuration for content security policy as shown in the code below the QR code for the Strapi:
1// Path: LaunchPad/strapi/config/middlewares.ts
2export default [
3 "strapi::logger",
4 "strapi::errors",
5 {
6 name: "strapi::security",
7 config: {
8 contentSecurityPolicy: {
9 useDefaults: true,
10 directives: {
11 "connect-src": ["'self'", "https:"],
12 "img-src": [
13 "'self'",
14 "data:",
15 "blob:",
16 "market-assets.strapi.io",
17 "qr.expo.dev",
18 ],
19 "media-src": [
20 "'self'",
21 "data:",
22 "blob:",
23 "market-assets.strapi.io",
24 "qr.expo.dev",
25 ],
26 upgradeInsecureRequests: null,
27 },
28 },
29 },
30 },
31 "strapi::cors",
32 "strapi::poweredBy",
33 "strapi::query",
34 "strapi::body",
35 "strapi::session",
36 "strapi::favicon",
37 "strapi::public",
38 "global::deepPopulate",
39];
Step 4: Modify the Next.js API Preview Handler
Head over to your Next.js app and locate the LaunchPad/next/app/api/preview/route.ts
file to handle the preview for the web.
1// Path: LaunchPad/next/app/api/preview/route.ts
2
3import { draftMode } from "next/headers";
4import { redirect } from "next/navigation";
5
6export const GET = async (request: Request) => {
7 // Parse query string parameters
8 const { searchParams } = new URL(request.url);
9 const secret = searchParams.get("secret");
10 const pathname = searchParams.get("pathname") ?? "/";
11 const status = searchParams.get("status");
12
13 // Check the secret and next parameters
14 // This secret should only be known to this route handler and the CMS
15 if (secret !== process.env.PREVIEW_SECRET) {
16 return new Response("Invalid token", { status: 401 });
17 }
18
19 if (status === "published") {
20 // Make sure draft mode is disabled so we only query published content
21 draftMode().disable();
22 } else {
23 // Enable draft mode so we can query draft content
24 draftMode().enable();
25 }
26
27 redirect(pathname);
28};
This is what we should now see.
We can now see the QR code of our Expo app. Of course, we are not setting up emulators or using EAS but to demonstrate how to preview our content on multiple frontends, and even on a native mobile app.
Handling Real-Time Preview Changes and Events
There is currently a problem with our preview. When we make an update, it is no longer going to be reflected.
This is because the content update works by Strapi (the blue box) dispatching an event to the window of the iframe. So the proxy (the green box) receives the event but the event no longer reaches the previews (the orange boxes).
To achieve real-time updates, you need to implement event listeners in your proxy component. These listeners will catch update events dispatched by Strapi and forward them to the appropriate preview iframe.
1// Path: LaunchPad/strapi/src/admin/PreviewProxy.tsx
2
3// ... imports
4
5const PreviewProxy = () => {
6 // ... component variables
7
8 // handle real-time changes
9 const iframe = React.useRef<HTMLFrameElement>(null);
10 React.useEffect(() => {
11 const handleMessage = (message) => {
12 if (message.data.type === "strapiUpdate") {
13 iframe.current?.contentWindow.postMessage(message.data, clientURL);
14 }
15 };
16 window.addEventListener("message", handleMessage);
17 return () => window.removeEventListener("message", handleMessage);
18 }, []);
19
20 return (
21 <Portal>
22 // ... other codes
23
24 // Attach ref to iframe
25 <Box
26 tag="iframe"
27 src={previewURL}
28 width={selectedDevice.width}
29 height={selectedDevice.height}
30 marginLeft="auto"
31 marginRight="auto"
32 display="block"
33 borderWidth={0}
34 ref={iframe}
35 />
36 </Portal>
37 );
38};
In the code above, we create a useEffect
hook function to set up a listener to listen to the message from Strapi. We set up a ref to an iframe so we can dispatch an event on it.
Creating Custom Preview Features
The sky's the limit when it comes to custom features. For example, you could implement a change highlighting feature that visually indicates which parts of the content have been updated. This can be incredibly useful for content editors working on large documents.
1// Highlighter Hook
2export function useUpdateHighlighter() {
3 const [searchParams] = useSearchParams();
4 const { kind, model, documentId, locale } = Object.fromEntries(searchParams);
5
6 const previousDocument = React.useRef<any>(undefined);
7 const iframe = React.useRef<HTMLIFrameElement>(null);
8
9 const { refetch } = useDocument({
10 collectionType:
11 kind === "collectionType" ? "collection-types" : "single-types",
12 model,
13 documentId,
14 params: { locale },
15 });
16
17 React.useEffect(() => {
18 const handleMessage = async (message: any) => {
19 if (message.data.type === "strapiUpdate") {
20 const response: any = await refetch();
21 const document = response.data.data;
22
23 let changedFields: Array<string> = [];
24 if (document != null && previousDocument.current !== document) {
25 // Get the diff of the previous and current document, find the path of changed fields
26 changedFields = Object.keys(document).filter(
27 (key) => document[key] !== previousDocument.current?.[key],
28 );
29 }
30
31 iframe.current?.contentWindow?.postMessage(
32 { ...message.data, changedFields },
33 new URL(iframe.current.src).origin,
34 );
35
36 previousDocument.current = document;
37 }
38 };
39
40 // Add the event listener
41 window.addEventListener("message", handleMessage);
42
43 // Cleanup the event listener on unmount
44 return () => {
45 window.removeEventListener("message", handleMessage);
46 };
47 }, [refetch]);
48
49 return { iframe };
50}
51
52
53// iframe inside PreviewProxy Component
54const { iframe } = useUpdateHighlighter();
Benefits of Strapi's Preview Feature: Why It's a Game-Changer
The Strapi Preview Feature offers several key benefits that make it a standout solution for content management workflows:
- Scalability: It can handle projects of varying complexities, from simple websites to complex multi-platform applications.
- Customizability: The feature provides the right primitives for you to build exactly what you need, rather than forcing you into a one-size-fits-all solution.
- Universal Compatibility: While the examples in this tutorial and the Strapi Preview docs often use Next.js, the preview feature works with any frontend framework that supports cookies and API endpoints. For example Astro, Remix, React Native, and so on.
GitHub Repo for Complete Code
The complete code for this tutorial can be found in this project repo.
Also, you can clone the strapiconf
branch of the LaunchPad repo to get the complete code.
Conclusion
In conclusion, the Strapi 5 Preview Feature represents a significant leap forward in content management capabilities. By providing a flexible, customizable, and powerful preview system, Strapi CMS can provide content teams to work more efficiently and effectively.
Whether you're managing a simple blog or a complex multi-platform content ecosystem, the Strapi Preview Feature offers the tools you need to deliver outstanding content experiences.
Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.
As part of the expansion team, Rémi's role is to help building and structuring the Strapi ecosystem, through projects like starters, plugins or the Strapi Market.