There is an ongoing shift in content management from traditional CMS to headless CMS. A headless CMS allows you to completely separate your content management system from the presentation layer. The content is made available via API and can be consumed in any kind of frontend, from websites to mobile apps.
Using headless CMS has opened up a new way of building websites, known as pre-rendering. It is one of the best-known techniques in Jamstack, in which the website is compiled into a set of static assets like pre-built HTML, CSS, and JavaScript files with the help of a static site generator (SSG). During the build time, the files are created by collecting the data from a headless CMS. These files are cached to a content delivery network (CDN) and served to a user on each request from the nearest CDN node. This improves speed and response times and reduces hosting costs.
However, content creators need to preview their content before publishing it to production, meaning they need to wait for an entire build to complete before they can view their content. To solve this problem, a preview mode allows editors to view their changes on the fly.
In this tutorial, you’ll learn to implement a preview system when working with a headless CMS like Strapi. You’ll implement the frontend in Next.js for creating content previews.
Here are what you’ll need to get started:
You’ll need a master directory that holds the code for both the frontend (Next.js) and backend (Strapi). Open your terminal, navigate to a path of your choice, and create a project directory by running the following command:
mkdir strapi-nextjs-previews
In the strapi-nextjs-previews
directory, you’ll install both Strapi and Next.js projects.
In your terminal, execute the following command to create the Strapi project:
npx create-strapi-app backend --quickstart --template @strapi/template-blog@1.0.0 blog
This creates a directory named backend
and contains all the code related to your Strapi project. It uses the preconfigured Strapi template for blogs.
Your Strapi project will start on port 1337 and open localhost:1337/admin/auth/register-admin in your browser. Set up your administrative user:
Enter your details and click the Let’s Start button.
You’ll be taken to the Strapi dashboard. Select the Content Manager header on the left sidebar, then, click the Article tab under the COLLECTION TYPES menu for pre-written articles you can access.
All the articles in your Strapi CMS are in the published state. Since the goal is to view the unpublished content, move some of the articles to the draft state.
Click the blog with id "1". You’ll be taken to the edit view screen:
Click the Unpublish button at the top of the screen to change the article into a draft.
Now, you have five published articles and one draft article. Next, you’ll learn how to fetch these articles via Strapi API and display them on a Next.js website.
It’s time to build the frontend website using Next.js. Since your current terminal window is serving the Strapi project, open another terminal window and execute the following command to create a Next.js project:
npx create-next-app frontend
This creates a directory named frontend
containing all the code related to the Next.js project. Navigate into the directory and start the Next.js development server by running the following commands:
cd frontend
npm run dev
This will start the development server on port 3000 and take you to localhost:3000. The first view of the Next.js website will look like this:
Now, add Bootstrap to style your blog. You can actually choose any design framework, but the installation steps may vary.
Shut down the Next.js development server by pressing Ctrl-C in your terminal and execute the following command to install bootstrap, react-bootstrap, and node-sass NPM packages for your Next.js website:
npm install bootstrap react-bootstrap node-sass --save
Once the installation is complete, open the next.config.js
file and add the following code:
1 const path = require('path');
2
3 module.exports = {
4 reactStrictMode: true,
5 sassOptions: {
6 includePaths: [path.join(__dirname, 'styles')],
7 },
8 };
This tells the node-sass to compile the SCSS to CSS from the styles
directory.
In the styles
directory:
globals.css
to globals.scss
.globals.scss
with the following line of code:1 @import '/node_modules/bootstrap/scss/bootstrap.scss';
In the pages
directory, open the _app.js
file and replace the existing code with the following:
1 import '/styles/globals.scss';
2
3 function MyApp({ Component, pageProps }) {
4 return <Component {...pageProps} />;
5 }
6
7 export default MyApp;
The above steps will complete the Bootstrap setup in your Next.js website.
You can learn more about customizing the Bootstrap in Next.js here.
In the Strapi CMS, the article content is written in Markdown, so you need a way to convert Markdown into HTML. For this, you can use the react-showdown NPM package.
To install react-showdown, execute the following command in your terminal:
npm install react-showdown --save
Now that you have set up the necessary packages for developing your Next.js website, you need to design an articles
page. It will fetch your articles from Strapi CMS and display them in the UI. In the pages
directory, create an articles
directory by running the following command:
mkdir pages/articles
In the articles
directory, create an index.js
file and add the following code:
1 import { Container, Row, Col } from 'react-bootstrap';
2 import { fetchArticlesApi } from '/lib/articles';
3 import Link from 'next/link';
4
5 const ArticlesView = (props) => {
6 // 2
7 const { articles } = props;
8 // 3
9 return (
10 <section className="py-5">
11 <Container>
12 <Row>
13 <Col lg="7" className="mx-lg-auto">
14 <h1 className="mb-5 border-bottom">Articles</h1>
15 {articles.data.map((article) => {
16 return (
17 <div key={article.id} className="mb-4">
18 <h2 className="h5">{article.attributes.title}</h2>
19 <p className="mb-2">{article.attributes.description}</p>
20 <Link href={'/articles/' + article.attributes.slug}>
21 <a className="">Read More</a>
22 </Link>
23 </div>
24 );
25 })}
26 </Col>
27 </Row>
28 </Container>
29 </section>
30 );
31 };
32
33 export async function getStaticProps() {
34 // 1
35 const articles = await fetchArticlesApi();
36 return {
37 props: { articles },
38 };
39 }
40
41 export default ArticlesView;
In the above code:
1. You use the fetchArticlesApi
function in the getStaticProps
function provided by Next.js. In the getStaticProps
function, you fetch the articles from the Strapi CMS and return them as a prop.
2. The articles
array is destructured from the prop
variable in the ArticlesView
.
3. The articles
are displayed in the UI.
The next step is to write code to fetch articles using Strapi API. For that, you need to implement the fetchArticlesApi
function.
Create the lib
directory under the frontend
directory by running the following command:
mkdir lib
In the lib
directory, create an articles.js
file and add the following code:
1 const STRAPI_URL = process.env.STRAPI_URL;
2
3 // Helper function to GET the articles from Strapi
4 async function fetchArticlesApi() {
5 const requestUrl = `${STRAPI_URL}/articles`;
6 const response = await fetch(requestUrl);
7 return await response.json();
8 }
9
10 // Helper function to GET a single article from Strapi
11 async function fetchArticleApi(slug) {
12 const requestUrl = `${STRAPI_URL}/articles/?filters\[slug\][$eq]=${slug}`;
13 const response = await fetch(requestUrl);
14 return await response.json();
15 }
16
17 // Helper function to GET a single article from Strapi which is in draft state
18 async function fetchArticlePreviewApi(slug) {
19 const requestUrl = `${STRAPI_URL}/articles?publicationState=preview&filters\[slug\][$eq]=${slug}`;
20 const response = await fetch(requestUrl);
21 return (await response.json())[0];
22 }
23
24 export { fetchArticlesApi, fetchArticleApi, fetchArticlePreviewApi };
In the above code, you have declared two functions:
fetchArticlesApi
function is used to fetch the published articles from the Strapi articles’ endpoint, localhost:1337/api/articles.fetchArticleApi
function is used to fetch a single article from Strapi based on the slug
passed as a parameter. It calls the Strapi single article endpoint, i.e. localhost:1337/api/articles/{slug}, and returns the fetched article.fetchArticlePreviewApi
function is used to fetch a single article when the preview mode is enabled. It calls the Strapi articles’ endpoint, ie localhost:1337/api/articles, and passes a query parameter publicationState
whose value is set to preview
. You use preview
when you want those articles that are either in published or draft state. Since you need only one article, the slug
parameter is used to search for an article by its slug. The response from Strapi API is an array, so you fetch the first item in the response
. Each article has a unique slug, so the response
array will contain a single item.Since you have referred to the STRAPI_URL
as an environment variable, you need to add it to a .env
file. To do so, create a .env
file at the root of your Next.js project and add the following secret to it:
1 STRAPI_URL=http://localhost:1337/api
Save your progress and start your Next.js development server by running:
npm run dev
Visit localhost:3000/articles and you’ll see your articles page rendered by Next.js:
Although you have six articles in Strapi CMS, you’ll only see five since one article is in draft state.
The next step is to design a single article page that needs to be dynamic. You can fetch your article using Strapi API based on the slug parameter provided in the URL path.
In the pages/articles
directory, create a [slug].js
file and add the following code:
1 import MarkdownView from 'react-showdown';
2 import { Container, Row, Col } from 'react-bootstrap';
3 import { fetchArticlesApi, fetchArticleApi } from '/lib/articles';
4
5 const ArticleView = (props) => {
6 const { article } = props;
7 return (
8 <section className="py-5">
9 <Container>
10 <Row>
11 <Col lg="7" className="mx-lg-auto">
12 <h1>{article.data[0].attributes.title}</h1>
13 <MarkdownView markdown={article.data[0].attributes.content} />
14 </Col>
15 </Row>
16 </Container>
17 </section>
18 );
19 };
20
21 // 1
22 export async function getStaticPaths() {
23 const articles = await fetchArticlesApi();
24 const paths = articles.data.map((article) => ({ params: { slug: article.attributes.slug } }));
25 return {
26 paths: paths,
27 fallback: false,
28 };
29 }
30
31 export async function getStaticProps(context) {
32 // 2
33 const slug = context.params.slug;
34 if (!slug) {
35 throw new Error('Slug not valid');
36 }
37
38 // 3
39 const article = await fetchArticleApi(slug);
40 if (!article) {
41 return { notFound: true };
42 }
43
44 // 4
45 return { props: { article } };
46 }
47
48 export default ArticleView;
In the above code:
1. You need to return a list of all possible values for the slug as Next.js needs to build a dynamic route. In the getStaticPaths
function, you make a call to the fetchArticlesApi
function, fetch all the published articles, and map their slugs to the path
array.
2. In the getStaticProps
function, you get the slug
from the URL params. If the slug
is not valid, you throw an error saying “Slug not valid.”
3. You fetch an article
based on the slug
parameter by calling the fetchArticleApi
. You redirect the user to the not found page in case the article is not valid.
4. The article
is passed as a prop, which is then used in the ArticleView
.
Save your progress. Click Read More on any article to open the single article page:
Now that your blog is designed, you need to configure the Next.js Preview API for viewing the drafts on your Next.js website.
In your Next.js project, add the following secret to the .env
file:
1 CLIENT_PREVIEW_SECRET=<long random string>
The CLIENT_PREVIEW_SECRET
variable is used to securely access the preview mode in the browser. It is passed as a query parameter to the preview endpoint when you want to enable the preview mode.
In the pages/api
directory, create a preview.js
file and add the following code:
1 import { fetchArticlePreviewApi } from '/lib/articles';
2
3 export default async function handler(req, res) {
4 // Get the preview secret and the slug which needs to be previewed
5 const secret = req.query.secret;
6 const slug = req.query.slug;
7 // If the secret passed as URL query parameter doesn't match the preview secret in .env
8 // then send a 401-Unauthorized response
9 if (secret !== process.env.CLIENT_PREVIEW_SECRET) {
10 return res.status(401).json({ message: 'Invalid token' });
11 }
12
13 // If slug is not provided, send a 400-Bad Request response
14 if (!slug) {
15 return res.status(400).json({ message: 'Parameter `slug` is not provided' });
16 }
17
18 // Send a request to Strapi and fetch the article
19 // to check if the provided slug exists
20 const article = await fetchArticlePreviewApi(slug);
21
22 // If the article is not found, send a 404-Not Found response
23 if (article === null) {
24 return res.status(404).json({ message: 'Article not found' });
25 }
26
27 // Enable Preview Mode by setting the cookies
28 res.setPreviewData({});
29
30 // Redirect to the path of the article slug
31 res.redirect(307, `/articles/${article.data[0].attributes.slug}`);
32 }
In the above code:
1. You destructure the secret
and slug
from the req.query
object.
2. You check whether the query parameter secret
matches the preview secret (CLIENT_PREVIEW_SECRET
).
3. You make sure that the slug
is not null
. In case it is, you send back a bad request response (res.status(400)
).
4. You fetch the article
associated with the requested slug by calling the fetchArticlePreviewApi
function. You pass the slug
parameter and fetch the articles in preview
mode.
5. You make sure that the fetched article
is not null
. In case it is, you send back a not found response (res.status(404)
).
6. After verifying the request, you call the res.setPreviewData({})
function, which is provided by the Next.js Preview API. It sets cookies in your browser which are valid for the current session.
7. You redirect (res.redirect()
) the user to the requested article page (/articles/${article.data[0].attributes.slug}
) using a 307 redirect.
The cookies set by preview mode API remain in the browser as long as you don’t close the browser window. You might need an API endpoint to turn off the preview mode manually and clear all cookies.
In the pages/api
directory, create an exit-preview.js
file and add the following code:
1 export default function handler(req, res) {
2 // Clear the cookies set by the preview mode.
3 res.clearPreviewData();
4
5 // Send a 200-Success response to the frontend
6 res.status(200).end();
7 }
In the above code:
1. You call the res.clearPreviewData()
function to delete the cookies set by the preview mode API.
2. You send back a success response using res.status(200).end()
.
Now you need to update the single article page to implement the preview mode functionality. Open the pages/articles/[slug].js
file and update it to match the following code:
1 import Router from 'next/router';
2 import MarkdownView from 'react-showdown';
3 import { Container, Row, Col } from 'react-bootstrap';
4 import { fetchArticlesApi, fetchArticleApi, fetchArticlePreviewApi } from '/lib/articles';
5
6 const ArticleView = (props) => {
7 // 4
8 const { article, previewMode } = props
9 return (
10 <section className="py-5">
11 <Container>
12 <Row>
13 <Col lg="7" className="mx-lg-auto">
14 {/* 5 */}
15 {previewMode ? (
16 <div className="small text-muted border-bottom mb-3">
17 <span>You are currently viewing in Preview Mode. </span>
18 <a role="button" className="text-primary" onClick={() => exitPreviewMode()}>
19 Turn Off Preview Mode
20 </a>
21 </div>
22 ) : (
23 ''
24 )}
25 <h1>{article.data[0].attributes.title}</h1>
26 <MarkdownView markdown={article.data[0].attributes.content} />
27 </Col>
28 </Row>
29 </Container>
30 </section>
31 );
32 };
33 // 6
34
35 async function exitPreviewMode() {
36 const response = await fetch('/api/exit-preview');
37 if (response) {
38 Router.reload(window.location.pathname);
39 }
40 }
41
42 export async function getStaticPaths() {
43 const articles = await fetchArticlesApi();
44 const paths = articles.data.map((article) => ({ params: { slug: article.attributes.slug } }));
45 return {
46 paths: paths,
47 fallback: false,
48 };
49 }
50
51 export async function getStaticProps(context) {
52 const slug = context.params.slug;
53 if (!slug) {
54 throw new Error('Article not found');
55 }
56
57 // 1
58 const previewMode = context.preview ? true : null;
59
60 // 2
61 let article;
62 if (previewMode) {
63 article = await fetchArticlePreviewApi(slug);
64 } else {
65 article = await fetchArticleApi(slug);
66 }
67
68 if (!article) {
69 return { notFound: true };
70 }
71
72 return {
73 props: {
74 article,
75 previewMode, // 3
76 }
77 };
78 }
79
80 export default ArticleView;
In the above code:
1. When you view a page in preview mode, the preview
property is set to true
and added to the context
object, which is passed as a parameter to the getStaticProps
function. Based on the value of context.preview
, you decide the value of previewMode
.
2. If previewMode
is true
, you fetch the article
associated with the requested slug in preview
mode by calling the fetchArticlePreviewApi
function; otherwise, you fetch the article using the fetchArticleApi
function.
3. You return the previewMode
as a prop along with the article
.
4. previewMode
is then destructured in the ArticleView
.
5. In the JSX template, if previewMode
is set to true
, you display a text stating that the preview mode is enabled and a button to close the preview mode. Clicking this button calls the exitPreviewMode
function.
6. The exitPreviewMode
function is used to turn off the preview mode by calling the exit preview API endpoint, ie /api/exit-preview
, then reloads the current article page after receiving a response.
Save your progress. Since you have added environment variables to your Next.js project, you need to restart your Next.js development server. Once the server restarts, visit http://localhost:3000/api/preview?secret=
<secret>
with the preview secret defined in the .env
file<slug>
with the slug of a draft articleIf you have passed the correct secret
and slug
query parameters, you will be redirected to http://localhost:3000/articles/
If you want to know more about the cookies set by the preview mode, open your browser’s Developer Tools and open the Cookies section. You’ll notice the __prerender_bypass
and __next_preview_data
cookies set in your browser:
At this point, you can test the exit-preview
API endpoint as well. Click Turn Off Preview Mode at the top of the page to exit the preview mode. It will delete all the cookies and refresh the page. You’ll be redirected to the 404 - Not Found page:
Visiting the preview URL directly is not handy, especially when it involves complex slugs and long preview secrets. The best thing to do is to add a preview button in Strapi Content Manager.
To do so, you need to create a preview button component in React and inject it into Strapi Content Manager. This is done by creating a Strapi plugin.
At the root (backend
) of the Strapi project, generate a plugin using the following command:
npm run strapi generate
preview-button
.JavaScript
for the plugin language.Make a PreviewLink
directory for your preview-button
plugin.
mkdir -p ./src/plugins/preview-button/admin/src/components/PreviewLink
Create an index.js
file in ./src/plugins/preview-button/admin/src/components/PreviewLink/
to provide a link for the Preview button.
1 // ./src/plugins/preview-button/admin/src/components/PreviewLink/index.js
2 import React from 'react';
3 import { useCMEditViewDataManager } from '@strapi/helper-plugin';
4 import Eye from '@strapi/icons/Eye';
5 import { LinkButton } from '@strapi/design-system/LinkButton';
6
7 const PreviewLink = () => {
8 const {initialData} = useCMEditViewDataManager();
9 if (!initialData.slug) {
10 return null;
11 }
12
13 return (
14 <LinkButton
15 size="S"
16 startIcon={<Eye/>}
17 style={{width: '100%'}}
18 href={`${CLIENT_FRONTEND_URL}?secret=${CLIENT_PREVIEW_SECRET}&slug=${initialData.slug}`}
19 variant="secondary"
20 target="_blank"
21 rel="noopener noreferrer"
22 title="page preview"
23 >Preview
24 </LinkButton>
25 );
26 };
27
28 export default PreviewLink;
The above code creates a PreviewLink
React component. In the StyledPreviewLink
component, you have specified a href
attribute that navigates the user to the preview API endpoint on the Next.js website. It makes reference to CLIENT_FRONTEND_URL
and CLIENT_PREVIEW_SECRET
, so you need to add these variables as environment variables.
To do so, open the .env
file at the root of the Strapi project and add the following environment variables:
1 CLIENT_PREVIEW_SECRET=<preview-secret>
2 CLIENT_FRONTEND_URL=http://localhost:3000
Add the same preview secret that you added in your Next.js project.
Since these variables need to be accessed on Strapi’s admin frontend, you need to add a custom webpack configuration.
Rename ./src/admin/webpack.config.example.js to ./src/admin/webpack.config.js. Refer to the v4 code migration: Updating the webpack configuration from the Official Strapi v4 Documentation for more information.
Edit ./src/admin/webpack.config.js. Refer to the Official webpack docs for more information: DefinePlugin | webpack.
1 // ./src/admin/webpack.config.js
2 'use strict';
3 /* eslint-disable no-unused-vars */
4 module.exports = (config, webpack) => {
5 // Note: we provide webpack above so you should not `require` it
6 // Perform customizations to webpack config
7 // Important: return the modified config
8 config.plugins.push(
9 new webpack.DefinePlugin({
10 CLIENT_FRONTEND_URL: JSON.stringify(process.env.CLIENT_FRONTEND_URL),
11 CLIENT_PREVIEW_SECRET: JSON.stringify(process.env.CLIENT_PREVIEW_SECRET),
12 })
13 )
14 return config;
15 };
The above code injects the CLIENT_FRONTEND_URL
and CLIENT_PREVIEW_SECRET
as global variables so that they can be used anywhere in the Strapi admin frontend.
Finally, to inject the PreviewLink
component in the Strapi Content Manager, edit the index.js
file at ./src/plugins/preview-button/admin/src/index.js
with the following code:
1 // ./src/plugins/preview-button/admin/src/index.js
2 import { prefixPluginTranslations } from '@strapi/helper-plugin';
3 import pluginPkg from '../../package.json';
4 import pluginId from './pluginId';
5 import Initializer from './components/Initializer';
6 import PreviewLink from './components/PreviewLink';
7 import PluginIcon from './components/PluginIcon';
8
9 const name = pluginPkg.strapi.name;
10 export default {
11 register(app) {
12 app.addMenuLink({
13 to: `/plugins/${pluginId}`,
14 icon: PluginIcon,
15 intlLabel: {
16 id: `${pluginId}.plugin.name`,
17 defaultMessage: name,
18 },
19 Component: async () => {
20 const component = await import(/* webpackChunkName: "[request]" */ './pages/App');
21 return component;
22 },
23
24 permissions: [
25 // Uncomment to set the permissions of the plugin here
26 // {
27 // action: '', // the action name should be plugin::plugin-name.actionType
28 // subject: null,
29 // },
30 ],
31 });
32
33 app.registerPlugin({
34 id: pluginId,
35 initializer: Initializer,
36 isReady: false,
37 name,
38 });
39 },
40
41 bootstrap(app) {
42 app.injectContentManagerComponent('editView', 'right-links', {
43 name: 'preview-link',
44 Component: PreviewLink
45 });
46 },
47
48 async registerTrads({ locales }) {
49 const importedTrads = await Promise.all(
50 locales.map(locale => {
51 return import(
52 /* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json`
53 )
54
55 .then(({ default: data }) => {
56 return {
57 data: prefixPluginTranslations(data, pluginId),
58 locale,
59 };
60 })
61
62 .catch(() => {
63 return {
64 data: {},
65 locale,
66 };
67 });
68 })
69 );
70 return Promise.resolve(importedTrads);
71 },
72 };
The above code injects the PreviewLink
component into the content manager.
Next enable the plugin by creating ./config/plugins.js
.
1 // ./config/plugins.js
2 module.exports = {
3 // ...
4 'Preview Button': {
5 enabled: true,
6 resolve: './src/plugins/preview-button' // path to plugin folder
7 },
8 // ...
9 }
All the above additions make changes to the Strapi admin frontend in React. You need to rebuild the admin panel.
To do so, shut down Strapi’s development server and rebuild the admin panel by running the following command:
npm run build
Once the rebuild is complete, start Strapi’s development server by running the following command:
npm run develop
Wait for the server to start and then visit localhost:1337/admin. Open any draft article. You’ll see a Preview button at the right-hand side of the content manager:
In case you’re not able to see the Preview button, delete the .cache
and build
directories by running rm -rf .cache build
, then rebuild your Strapi admin frontend by running npm run build
.
At this point, you can test whether the preview button works. Update the content of your draft article and click Save.
Click the Preview button. You’ll be directed to the single article page on your Next.js website.
Great work! One of the best things of Strapi is that it is open-source. Even if it doesn’t have a certain feature, you can customize Strapi according to your needs. This is exactly what you did today. Your content editors will love the preview mode because they can use it to view their content without building the entire website.
For a similar option, you can implement Previews in a Nuxt.js app using Strapi as a backend. The entire source code for this tutorial is available in this GitHub repository.
Mark Munyaka is a freelance web developer and writer who loves problem-solving and testing out web technologies. You can follow him on Twitter @McMunyaka