Quadri Sheriff, an aspiring technical writer, software programmer, and Amir Tadrisi, a full-stack engineer who love the challenges of working with cutting-edge technologies both wrote great articles; Implementing Previews with Next Applications using a Strapi backend & How to use Image and preview in your Nextjs - Strapi blog about using the preview system of Next.js with Strapi.
I want to take this opportunity to thank them because this use case is very requested and their articles answer a lot of common questions. I want to emphasize that this type of contribution is very important for the Strapi community and if you wish to be part of it, Feel free to join our Write for the Community Program.
This (very) short article complements what Quadri and Amir wrote. You'll see how to implement a preview button directly in the Strapi admin. Pretty cool right?
You must surely know by now that Strapi is completely open-source. If you want to include a button in your Content Manager that generates a random GIF, know that it is absolutely doable. Simply create your component and inject it into the admin.
Injecting a component is exactly what we're going to do here.
Quick Warning, this tutorial is based on the v3 of Strapi. A version of this tutorial for the v4 version will be mentioned at the top of the page when it is made.
As mentioned earlier I'm going to assume that you already have a preview system on your favorite front-end framework like Next.js that requires a specific preview URL like this:
1http://localhost:3000/api/preview?secret=fdsfasdmgrNPQXtfdsfMswfdsfdsfasdkjfow&slug=pricing](http://localhost:3000/api/preview?secret=fdsfasdmgrNPQXtfdsfMswfdsfdsfasdkjfow&slug=pricing&lang=en&type=page
We'll simply create a button in the Content-Manager that will build this URL based on your collection-type.
The /extensions
folder contains all the plugin customizable files. Find more information on the dedicated documentation page.
You should have a content-manager
folder like below:
./extensions/content-manager/admin/src/index.js
file containing the following:1import pluginPkg from '../../package.json';
2import pluginId from './pluginId';
3import pluginLogo from './assets/images/logo.svg';
4import App from './containers/Main';
5import ExternalLink from './InjectedComponents/ExternalLink';
6import ConfigureViewButton from './InjectedComponents/ContentTypeBuilder/ConfigureViewButton';
7import lifecycles from './lifecycles';
8import reducers from './reducers';
9import trads from './translations';
10
11export default (strapi) => {
12 const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
13 const plugin = {
14 blockerComponent: null,
15 blockerComponentProps: {},
16 description: pluginDescription,
17 icon: pluginPkg.strapi.icon,
18 id: pluginId,
19 initializer: null,
20 injectedComponents: [
21 {
22 plugin: 'content-type-builder.listView',
23 area: 'list.link',
24 component: ConfigureViewButton,
25 key: 'content-manager.link',
26 },
27 // This is the injection zone
28 {
29 plugin: 'content-manager.editView',
30 area: 'right.links',
31 component: ExternalLink,
32 key: 'content-manager.preview-link',
33 },
34 ],
35 injectionZones: {
36 editView: { informations: [] },
37 listView: { actions: [], deleteModalAdditionalInfos: [] },
38 },
39 isReady: true,
40 isRequired: pluginPkg.strapi.required || false,
41 layout: null,
42 lifecycles,
43 mainComponent: App,
44 name: pluginPkg.strapi.name,
45 pluginLogo,
46 preventComponentRendering: false,
47 reducers,
48 trads,
49 };
50
51 return strapi.registerPlugin(plugin);
52};
This file allows you to inject a custom component that you are about to create. Just import the following component::
1import ExternalLink from './InjectedComponents/ExternalLink';
And then you inject it:
1injectedComponents: [
2 {
3 plugin: 'content-type-builder.listView',
4 area: 'list.link',
5 component: ConfigureViewButton,
6 key: 'content-manager.link',
7 },
8 // This is the injection zone
9 {
10 plugin: 'content-manager.editView',
11 area: 'right.links',
12 component: ExternalLink,
13 key: 'content-manager.preview-link',
14 },
15],
You can find more information about it on our documentation.
./extensions/content-manager/admin/src/InjectedComponents/ExternalLink/index.js
file containing the following:1import React from 'react';
2import styled from 'styled-components';
3import { useContentManagerEditViewDataManager } from 'strapi-helper-plugin';
4import EyeIcon from './view.svg';
5
6const StyledExternalLink = styled.a`
7 display: block;
8 color: #333740;
9 width: 100%;
10 text-decoration: none;
11 span,
12 i,
13 svg {
14 color: #333740;
15 width: 13px;
16 height: 12px;
17 margin-right: 10px;
18 vertical-align: 0;
19 }
20 span {
21 font-size: 13px;
22 }
23 i {
24 display: inline-block;
25 background-image: url(${EyeIcon});
26 background-size: contain;
27 }
28 &:hover {
29 text-decoration: none;
30 span,
31 i,
32 svg {
33 color: #007eff;
34 }
35 }
36`;
37
38const ExternalLink = () => {
39 const { modifiedData, layout } = useContentManagerEditViewDataManager();
40
41 if (modifiedData.published_at) {
42 return null;
43 }
44
45 if (!modifiedData.slug) {
46 return null;
47 }
48
49 if (!CLIENT_URL || !CLIENT_PREVIEW_SECRET) {
50 return null;
51 }
52
53 return (
54 <li>
55 <StyledExternalLink
56 href={`${CLIENT_URL}/api/preview?secret=${CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}&locale=${modifiedData.locale}&apiID=${layout.apiID}&kind=${layout.kind}`}
57 target="_blank"
58 rel="noopener noreferrer"
59 title="page preview"
60 >
61 <i />
62 Preview
63 </StyledExternalLink>
64 </li>
65 );
66};
67
68export default ExternalLink;
There is a lot of stuff to unpack and explain here. The first thing is that you import an SVG eye icon for the button.
./extensions/content-manager/admin/src/InjectedComponents/ExternalLink/view.svg
file containing the following:1<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
Now, let's focus on this section:
1const ExternalLink = () => {
2 const { modifiedData, layout } = useContentManagerEditViewDataManager();
3
4 if (modifiedData.published_at) {
5 return null;
6 }
7
8 if (!modifiedData.slug) {
9 return null;
10 }
11
12 if (!CLIENT_URL || !CLIENT_PREVIEW_SECRET) {
13 return null;
14 }
15
16 return (
17 <li>
18 <StyledExternalLink
19 href={`${CLIENT_URL}/api/preview?secret=${CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}&locale=${modifiedData.locale}&apiID=${layout.apiID}&kind=${layout.kind}`}
20 target="_blank"
21 rel="noopener noreferrer"
22 title="page preview"
23 >
24 <i />
25 Preview
26 </StyledExternalLink>
27 </li>
28 );
29};
This is the code that is implemented on FoodAdvisor, our live demo which has a Next.js front-end. First of all we get modifiedData
and layout
from useContentManagerEditViewDataManager
hook. The first is an object that contains your entry data, and the second has more interesting information about your entry (apiID, kind of content-type, metadata, etc...)
Nothing happens if the content is already published. If there is no slug
field or if the necessary env variables don't exist. In fact, we only want to show the preview button when the content has not been published yet. Also, if it doesn't contain a slug or the necessary env variables, it means that you will not be able to preview it on your front-end.
1if (modifiedData.published_at) {
2 return null;
3}
4
5if (!modifiedData.slug) {
6 return null;
7}
8
9if (!CLIENT_URL || !CLIENT_PREVIEW_SECRET) {
10 return null;
11}
Then if there is no problem, we simply render the button in the edit view of your content-manager:
1return (
2 <li>
3 <StyledExternalLink
4 href={`${CLIENT_URL}/api/preview?secret=${CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}&locale=${modifiedData.locale}&apiID=${layout.apiID}&kind=${layout.kind}`}
5 target="_blank"
6 rel="noopener noreferrer"
7 title="page preview"
8 >
9 <i />
10 Preview
11 </StyledExternalLink>
12 </li>
13 );
Please take a moment to see how the url is built:
1`${CLIENT_URL}/api/preview?secret=${CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}`
Nothing really complicated here, It simply takes the URL of your client application and It appends the right path (here for Next.js): /api/preview?secret=xxx&slug=xxx
You probably already have the file in your Next.js application that will handle this call. To give you an idea, this is what we do on FoodAdvisor:
1// ./pages/api/preview.js
2
3import { getData } from '../../utils';
4
5export default async (req, res) => {
6 if (
7 req.query.secret !== process.env.PREVIEW_SECRET ||
8 (req.query.slug != '' && !req.query.slug)
9 ) {
10 return res.status(401).json({ message: 'Invalid token' });
11 }
12
13 const previewData = await getData(
14 req.query.slug,
15 req.query.locale,
16 req.query.apiID,
17 req.query.kind,
18 null
19 );
20
21 if (!previewData.data) {
22 return res.status(401).json({ message: 'Invalid slug' });
23 }
24 res.setPreviewData({});
25
26 res.writeHead(307, {
27 Location: previewData.slug,
28 });
29
30 res.end();
31};
For FoodAdvisor, I needed to have more information concerning the data I wanted to preview in order to get more flexibility and add parameters such as the locale, the apiID, and the kind of content-types. This is doable by simply adding more params to the URL:
1`${CLIENT_URL}/api/preview?secret=${CLIENT_PREVIEW_SECRET}&slug=${modifiedData.slug}&locale=${modifiedData.locale}&apiID=${layout.apiID}&kind=${layout.kind}`
This is why I needed to have the layout object but you can remove it if you don't need it. I will not detail why I needed these fields in this article but feel free to check the source code of FoodAdvisor to get a better understanding of the approach:
Well, there is nothing else to do! You can build your admin, run your server and preview your content!
1yarn build --clean
1yarn develop
Now, after creating an entry or by just filling in the slug field, you can see a preview button on the right-hand side of your Content Manager.
This is what it will look like on the FoodAdvisor front-end:
Customizing Strapi can be very easy. We only created 3 files here, and one of them is a .svg!
Strapi doesn't provide a native preview feature since it is a headless CMS but by being open-source, it gives you the freedom and flexibility to adapt it for the needs of your front-end application.
This is one of the easiest implementations of a preview you can do in the Strapi admin. I challenge you to go further by creating a component that will directly display the front-end application previews in the Content Manager!
See you in the next article!
Maxime started to code in 2015 and quickly joined the Growth team of Strapi. He particularly likes to create useful content for the awesome Strapi community. Send him a meme on Twitter to make his day: @MaxCastres