How to Implement Previews with Next Applications using a Strapi backend
Tags: strapi, nextjs, headlesscms, customization
Published: 2022-09-19 07:28:55

**Author: Mark Munyaka**

In this guide, you will learn about the importance of previews in [Jamstack](https://jamstack.org/headless-cms/strapi/). I will also walk you through the process of adding the previews mode to a blog built with [Next.js](https://strapi.io/integrations/nextjs-cms) and [Strapi headless CMS](https://strapi.io) for a better content editing experience.

## The Preview Mode
One of the major features of the Jamstack web development architecture is its core principle of pre-rendering, i.e., generating the website's content during build time instead of every request time. Pre-rendering improves the speed, reduces the complexity, and reduces the cost of hosting the website.

The pre-rendering process in the Jamstack web development methodology is achieved with the help of a static site generator(SSG). An SSG collects content from a headless CMS, where the files are created and stored, with the help of an API.

The contents are then converted into static files (prebuilt HTML, CSS, and Javascript files) by the SSG during the SSG build time. Finally, the generated static files can be deployed to a production server or stored in a content delivery network(CDN) and served as a whole to the end-user in every request.

Even though it is very beneficial, this pre-rendering process tends not to be ideal during the content creation process. Content creators and editors need to render a preview of their draft at every request time instead of waiting for the complete website build process before viewing their changes.

The easiest solution to the problem conferred by most static site generators is setting up a **Preview Mode** that will bypass the static site generator pre-rendering process. With the preview mode activated, content editors can view their changes on every page request instead of waiting for the complete build process to run.

## Setting up the Preview Mode
For the sake of this tutorial, we will create a simple blog website for which we will later activate the previews mode to allow for a better editing experience.

## Prerequisites
To follow through with the guide, you will need a basic knowledge of:
- [Next.js](https://strapi.io/integrations/nextjs-cms): Next.js is our static site generator of choice for this tutorial. Next.js is an open-source react framework used for building production-grade react websites. It comes with a preview API that we will use in this guide.
    
- [Tailwind CSS](https://tailwindcss.com/): Tailwind CSS is a utility-first CSS framework. It enables you to add CSS directly to your HTML for easier development and scaling.
    
- [Strapi](https://strapi.io/) v4: Strapi is the leading open-source headless CMS used for managing content efficiently and designing secure production-ready Restful or GraphQL APIs. 
    
- [Markdown-to-jsx](https://www.npmjs.com/package/markdown-to-jsx): We will use the Markdown-to-jsx npm package for converting the markdown files returned from Strapi into jsx to be rendered by Next.js.

This tutorial assumes you have the following system configuration:

- **Operating System**: Unix/Linux, macOS or Windows
- **Version Control**: Git
- **Shell**: Any Unix Shell or Git Bash for Windows
- **Text Editor**: Any (I used VSCode)
- **Node**: v12.xx to v16.xx
- **Package Manager**: npm or yarn (npm comes with Node)

## Strapi Setup
The first step in this guide will be to create the backend of our blog. We will be using the [strapi blog template](https://github.com/strapi/starters-and-templates/tree/main/packages/templates/blog) as a starter for faster and easier development.

```bash
    npx create-strapi-app backend  --quickstart --template @strapi/template-blog@1.0.0 blog 
```
[View the blog template here.](https://github.com/strapi/starters-and-templates/tree/main/packages/templates/blog)

This command will create a new backend folder with the Strapi backend fully set up and all dependencies installed during installation.

After the successful installation, you will automatically be redirected to your browser, where you will be prompted to register before using Strapi. If not redirected, visit http://localhost:1337/admin in your browser.

![Strapi Admin Registration Form](https://lh3.googleusercontent.com/HYANaaGn0RcvLDbI0Hr21H-R52pHj44x_0r113trp13x8JOkeqDdtXF_RTlx0Dpw9drPXOjs_Rc9EI_y8x--BRFd2UEPARBFSufDQhMo80PaijCGuR-x614nP9eFbre7-UQkUGExNNSK9ajdjg)

Add your details and click **LET’S START** to access the Strapi dashboard. Navigate to the **Content Manager.** Select **Article** under **COLLECTION TYPES**, and you will see a list of articles.

![List of articles in the Strapi dashboard](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659612432693_list-of-articles-in-strapi-dashboard.png)

The blog template for Strapi comes with some preconfigured schema for blogging apps, making our development easier and faster.

Set the `STATE` of the articles in **COLLECTION TYPES** to either `Published` or `Draft`. First, click on the pen symbol next to **Published.** 

![Select Edit item icon](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659612757207_click-on-pen-icon-for-listed-article.png)

Click **Unpublish.** Choose **Yes, confirm** in the Confirmation dialog box, then **Save** to change the state of the article.

![Click on Unpublish then Select Save](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659613155022_click-unpublish-then-save-article.png)

Repeat the procedure for the articles you want to make drafts and your list of articles should be similar to this:

![List of articles including drafts](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659613602918_list-of-articles-which-includes-drafts.png)

## Next.js Setup

The next step in this guide will be the frontend setup with [Next.js](https://strapi.io/integrations/nextjs-cms). First, initialize your frontend project with this command.

```bash
    npx create-next-app frontend
```
A **frontend** folder is created in your root directory. Navigate into the folder and run `npm run dev` to start the frontend server. You can view your frontend locally using [http://localhost:3000](http://localhost:3000/).

![Next.js frontend homepage](https://lh6.googleusercontent.com/W2DQKhala-F-ca1Jj5mWKrP5xK5c4853Yk3AVm-All12hxT7pXcf_NssiIszcnacXCFQla8g6ElMrAg32I_DQ41jSDQboQY9qgq_dUh2ouhthZeea3PdRfMoNYUVVb0hTwzxeEDF)

## Tailwind CSS Setup
Stop your frontend server by pressing `Ctrl` plus `C` on your keyboard. Install Tailwind CSS into your frontend with npm.

```bash
    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
```

Then, generate tailwind CSS configuration files.
```bash
    npx tailwindcss init -p
```

This command will generate a `postcss.config.js` and a `tailwind.config.js` file to the root directory of your project. We will use these two files for any configurations relating to tailwind CSS in our project.

Navigate to the `pages/_app.js` file and replace import `'../styles/globals.css'` with `import 'tailwindcss/tailwind.css'`. You can delete the `styles` folder since we will not be using any custom CSS for our blog.

Next, add the paths to all of your template files in your `tailwind.config.js` file. 

```js
    /* ./tailwind.config.js */
    /** @type {import('tailwindcss').Config} */ 
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
```
Complete your Tailwind CSS setup by adding the `@tailwind` directives for each of Tailwind’s layers to your `./styles/globals.css` file.

```css
    /* ./styles/globals.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
```
For more information on installing Tailwind CSS, refer to the [Install Tailwind CSS with Next.js guide](https://tailwindcss.com/docs/guides/nextjs).

## Markdown-to-jsx Installation

Install the **markdown-to-jsx** package into your frontend using the following command:

```bash
    npm install markdown-to-jsx
```

The **markdown-to-jsx** npm package will be used to convert the markdown files obtained from Strapi into JSX files to be rendered in our frontend framework.

## Connecting Frontend to Backend
To connect our Next.js frontend app to our backend, we will create a helper function that will make API calls to the Strapi backend with the fetch API.

To create the helper function, create a `lib` folder in your frontend root folder, and add a file named `api.js` to the `lib` folder.

Add the following code lines to the **api.js** file:

```js
    /* ./lib/api.js */
    export function getStrapiURL(path = "") {
      return `${process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337/api"
            }${path}`;
    }
    
    // Helper to make GET requests to Strapi
    export async function fetchAPI(path) {
        const requestUrl = getStrapiURL(path);
        const response = await fetch(requestUrl);
        const data = await response.json();
        return data;
    }
```
This code sends a `GET` request to Strapi URL with the fetch API. Since we are only building a blog, a helper to fetch the content using `GET` requests is enough for our project.

## Designing the Blog Layout

Now that we have our frontend and backend connected and Tailwind CSS fully installed, the next step will be to create a layout for our blog.

Firstly, create a `components` folder in the root folder and add a `layout.js` file into the folder. Next, add the following code into the `./components/layout.js` file:

```js
    /* ./components/layout.js */
    import Header from './header';
    import Footer from './footer';
    
    const Layout = ({ children }) => (
        <>
            <Header />
            {children}
            <Footer />
        </>
    );
    
    export default Layout;
```
Then, we will create our header and footer files. First, add a `header.js` and a `footer.js` file to the `components` folder.

Add the following code to your `./components/header.js` file:

```js
    /* ./components/header.js */
    import React from "react";
    import Link from "next/link";
    
    const Header = () => {
        return (
            <div>
                <nav className="flex justify-center items-center h-16 py-2 mb-2 bg-gray-100 w-full border-b">
                    <div>
                        <Link href="/">
                            <a href="/" className="px-2 font-black lg:px-0 text-lg">
                                Sybox Digital Blog
                            </a>
                        </Link>
                    </div>
                </nav>
            </div>
        );
    };
    
    export default Header;
```
Then, add the following code to the `./components/footer.js` file.

```js
    /* ./components/footer.js */
    import React from "react";
    
    const Footer = () => {
        return (
            <footer className="border-t mt-10 pt-12 pb-32 px-4 lg:px-0">
                <div className="px-4 pt-3 pb-4 border-b -mx-4 border-gray-100">
                    <div className="max-w-xl mx-auto">
                        <h2 className="text-xl text-left inline-block font-semibold text-gray-800">Subscribe to Our Newsletter</h2>
                        <div className="text-gray-700 text-xs pl-px">
                            Latest news, articles and monthly updates delivered to your inbox.
                        </div>
                        <form action="#" className="mt-2">
                            <div className="flex items-center">
                                <input type="email" className="w-full py-4 mr-2  bg-gray-100 shadow-inner rounded-md border border-gray-400 focus:outline-none" required />
                                <button className="bg-blue-600 text-gray-200 text-sm rounded">Sign Up</button>
                            </div>
                        </form>
                    </div>
                </div>
            </footer>
        );
    };
    
    export default Footer;
```
The layout component will be wrapped around our page code to give a uniform header and footer across our blog.

## Designing the Homepage
Move to your `./pages/index.js` file and discard the pre-existing code there. Then add the following lines of code to the `./pages/index.js` file:

```js
    /* ./pages/index.js */
    import Link from "next/link";
    import Layout from "../components/layout";
    import { fetchAPI } from "../lib/api";
    
    export default function Home({ articles }) {
      return (
        <>
          <Layout>
            <body className="antialiased md:bg-gray-100">
              <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
                {articles.data.map((article) => (
                  <div key={article.id} className="md:p-8 p-2 bg-white">
                    <div>
                      {article.attributes.image && (
                        <img src={`http://localhost:1337${article.attributes.image.data.attributes.url}`} />
                      )}
                    </div>
                    {article.attributes.title}
                    <div>
                      {article.attributes.category && (
                        <div className="text-indigo-500 font-semibold text-base mt-2">
                          {article.attributes.category.data.attributes.name}
                        </div>
                      )}
                    </div>
                    <h1 className="font-semibold text-gray-900 leading-none text-xl mt-1 capitalize truncate">
                      {article.attributes.title}
                    </h1>
                    <Link href={`/article/${article.attributes.slug}`}>
                      <div className="max-w-full">
                        <div className="text-base font-medium tracking-wide text-gray-600 mt-1">
                          {article.attributes.description}
                        </div>
                      </div>
                    </Link>
                    <div className="flex items-center space-x-2 mt-20">
                      <div>
                        {article.attributes.author && (
                          <div className="text-gray-900 font-semibold pb-2">
                            {article.attributes.author.data.attributes.name}
                          </div>
                        )}
                        <div className="text-gray-500 font-semibold text-sm">
                          Created on - {new Date(article.attributes.createdAt).toDateString()}
                        </div>
                        <div className="text-gray-500 font-semibold text-sm">
                          Updated on - {new Date(article.attributes.updatedAt).toDateString()}
                        </div>
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            </body>
          </Layout>
        </>
      );
    }
    export async function getStaticProps() {
      try {
        const articles = await fetchAPI("/articles?populate=*");;
        return {
          props: {
            articles
          },
        };
      } catch (error) {
        return { error };
      }
    }
```
In this code, we exported articles with the published status from the [Strapi backend](http://localhost;1337) with `getStaticProps()`. The `getStaticProps()` is a Next.js API that will pre-render our page at build time using the props returned in it.

We then pass the `props(articles)` returned by `getStaticProps()` into the `<Home />` component. The props are then rendered in a UI built with Tailwind CSS. We will also wrap the page in the `<Layout />` component to access our pre-made header and footer components.

**Start your frontend server:**
```bash
    npm run dev`
```

The [homepage](http://localhost:3000) on http://localhost:3000 should be similar to this:

![Next Blog with Published Articles](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659620854038_next-blog-home-page-showing-published-articles.png)

## Designing the Blog Post Page
We will design a dynamic route to render each blog post page based on their slug in this step. Thus, the URL of each page will be dependent on the slug of the page.

Stop your server if you haven’t already. Create an `article` folder in the `pages` folder, and add a `[slug].js` file to the `./pages/article` folder.

Add the following lines of code to the `./pages/article/[slug].js` file:

```js
    /* ./pages/article/[slug].js */
    import Markdown from "markdown-to-jsx";
    import { fetchAPI } from "../../lib/api";
    import Layout from "../../components/layout";
    const Article = ({ article }) => {
     return (
       <>
         <Layout>
           <div className="mt-10">
             <div className="mb-4 md:mb-0 w-full max-w-screen-md mx-auto">
               <div className="absolute left-0 bottom-0 w-full h-full" />
              <div>
                 {article.data[0].attributes.image && (
                  <img src={`http://localhost:1337${article.data[0].attributes.image.data.attributes.url}`} />
                )}
               </div>
               <div>
                 {article.data[0].attributes.category && (
                   <a
                     href="#"
                     className="px-4 py-1 bg-black text-blue-200 inline-flex text-md items-center justify-center mb-2"
                   >
                     {article.data[0].attributes.category.data.attributes.name}
                   </a>
                 )}
               </div>
               <h2 className="text-4xl pt-2 font-semibold text-gray-500 leading-tigh`t">
                 {article.data[0].attributes.description}
               </h2>
              <div className="mt-3">
                 {article.data[0].attributes.author && (
                   <p className="text-blue-900 font-semibold pb-2">
                    Written by - {article.data[0].attributes.author.data.attributes.name}
                   </p>
                )}
               </div>
             </div>
             <article className="prose lg:prose-xl px-4 lg:px-0 mt-12 text-gray-700 max-w-screen-md mx-auto text-lg leading-relaxed">
               <Markdown>{article.data[0].attributes.content}</Markdown>
             </article>
           </div>
         </Layout>
       </>
     );
    };
    export default Article;
    export async function getStaticPaths() {
     const articles = await fetchAPI("/articles?populate=*");
     return {
       paths: articles.data.map((article) => ({
         params: {
          slug: article.attributes.slug,
        },
       })),
       fallback: false,
     };
    }
    export async function getStaticProps({ params }) {
     const article = await fetchAPI(`/articles/?filters\[slug\][$eq]=${params.slug}&populate=*`);
     return {
       props: { article },
       revalidate: 1,
     };
    }
```

This gets a single article with its slug and displays the article. So, first, we query our backend for every article with the `getStaticPaths()` API. Then, we get the slug and store them in the slug `params`.

The slug `params` are then passed into the `getStaticProps()` API. The `getStaticProps()` function queries the backend with a single slug and returns the data collected from the backend as props. The props data are then passed into the `< Article />` component and rendered with our UI created with tailwind CSS.

**Start your frontend server:**
```bash
    npm run dev
```

Visit one of the published articles by entering [Sample Published Articles](http://http:l//localhost:1337/article/what-s-inside-a-black-hole) in your browser. Replace `${slug}` with a **slug** to one of the posts, for example http://localhost:3000/article/what-s-inside-a-black-hole. 

Each blog post should appear similar to this:

![Single Blog Post](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659706527251_Single+article+on+Next+Blog.png)

Now that our blog is completely designed, we can now configure the preview API for a more effortless content editing experience on our platform.

## Activating the Preview Mode
The first step will be to create a preview secret. Then, we will use this secret to access the preview mode securely in our browser. Create an **.env** file in the root of your **frontend** folder and add your preview secret to the file.
```
PREVIEW_SECRET=****
```
Replace ****** with a unique string of characters.

Next, we will add a helper function to our `./lib/api.js` file. We will use this helper to query our backend for preview data.

```js
    /* ./lib/api.js
     *
     * Add this code below the last line of code
     */    
    
    export async function getPageData(slug, preview = false) {
        // Find the pages that match this slug
        const pagesData = await fetchAPI(
            `/articles?publicationState=preview&filters\[slug\][$eq]=${slug}&populate=*`
        );
        // Make sure we found something, otherwise return null
        if (pagesData == null || pagesData.length === 0) {
            return null;
        }
        // Return the first item since there should only be one result per slug
        return pagesData;
    }
```
The code queries the Strapi for the requested slug and returns either published or draft blog posts.

The next step will be to create and configure the preview API route. In `./pages/api`, add a new file called `preview.js`. Then, add the following code to the file:

```js
    /* ./pages/api/preview.js */
    
    import { getPageData } from "../../lib/api"
    
    export default async function handler(req, res) {
        if (req.query.secret !== (process.env.PREVIEW_SECRET || 'secret-token')) {
            return res.status(401).json({ message: "Invalid token" });
        }
    
        const pageData = await getPageData(req.query.slug, true);
    
        if (!pageData) {
            return res.status(401).json({ message: "Invalid slug" });
        }
    
        res.setPreviewData({});
    
        res.writeHead(307, { Location: `/article/${req.query.slug}` });
        res.end();
    };
```

This code matches the secret in the preview URL to the preview secret in the `.env` file. After verifying the preview request, the API then activates the preview mode and redirects to the article location.

When the preview mode is activated, Next.js sets some cookies in your browser. We will add these cookies to any request going to Next.js automatically, and Next.js treats any request with this cookie as a preview mode request.

Go back to your `[slug].js` file, import the **getPageData** helper from the `./lib/api` page:
```js
import { fetchAPI, getPageData } from "../../lib/api";
```
Change your `getStaticProps()` code as follows:

```js
    /* ./pages/article/[slug].js
     *
     * Change getStaticProps()
     */
    export async function getStaticProps({ params, preview = null }) {
        const article = await getPageData(params.slug, preview);
       
        return {
          props: { article, preview },
          revalidate: 1,
        };
       } 
```

## Accessing the Preview Mode

Access the preview mode by navigating to this url [http://localhost:3000/api/preview?secret=<secret>&slug=<slug>](http://localhost:3000/api/preview?secret=abc&slug=what-s-inside-a-black-hole)

Where:

<secret> = secret token defined in your `.env` file
<slug> = the draft page slug.

## Deactivating the Previews Mode
Navigate to the `./pages/api/` folder and create a new file named `exit-preview.js`. Next, add the following lines of code to the `exit-preview.js` file:

```js
    /*. /pages/api/exit-preview.js */
    
    export default function handler(req, res) {
    
        try {
            // Clears the preview mode cookies.
            // This function accepts no arguments.
            res.clearPreviewData()
    
            return res.status(200).json({ message: 'Cookies Cleared' })
        } catch (error) {
            return res.status(500).json({ message: error })
        }
    }
```

This code clears all the cookies set by the previews mode API in your browser. Also, navigate to `./pages/articles/[slug].js` and change the `<Article />` component.

```js
    /* ./pages/articles/[slug].js */
    
    import Markdown from "markdown-to-jsx";
    import { fetchAPI, getPageData } from "../../lib/api";
    import Layout from "../../components/layout";
    
    const Article = ({ article, preview }) => {
      return (
        <>
    
          /* Section of code to add */
          /* Beginning of Section */
          <div>
            {preview ? (
              <div className="relative bg-indigo-600">
                <div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
                  <div className="pr-16 sm:text-center sm:px-16">
                    <p className="font-medium text-white">
                      <span>Preview mode is on,</span>
                      <span className="block sm:ml-2 sm:inline-block">
                        <a
                          href="http://localhost:3000/api/exit-preview"
                          className="underline hover:text-cyan transition-colors"
                        >
                          turn off
                        </a>
                      </span>
                    </p>
                  </div>
                </div>
              </div>
            ) : null}
          </div>
          /* End of section */
    
          <Layout>
            <div className="mt-10">
              <div className="mb-4 md:mb-0 w-full max-w-screen-md mx-auto">
                <div className="absolute left-0 bottom-0 w-full h-full" />
                <h1 className="text-6xl font-bold pb-4 text-center">{article.data[0].attributes.title}</h1>
                <div>
                  {article.data[0].attributes.image && (
                    <img src={`http://localhost:1337${article.data[0].attributes.image.data.attributes.url}`} />
                  )}
                </div>
                <div>
                  {article.data[0].attributes.category && (
                    <a
                      href="#"
                      className="px-4 py-1 bg-black text-blue-200 inline-flex text-md items-center justify-center mb-2"
                    >
                      {article.data[0].attributes.category.data.attributes.name}
                    </a>
                  )}
                </div>
                <h2 className="text-4xl pt-2 font-semibold text-gray-500 leading-tight">
                  {article.data[0].attributes.description}
                </h2>
                <div className="mt-3">
                  {article.data[0].attributes.author && (
                    <p className="text-blue-900 font-semibold pb-2">
                      Written by - {article.data[0].attributes.author.data.attributes.name}
                    </p>
                  )}
                </div>
              </div>
              <article className="prose lg:prose-xl px-4 lg:px-0 mt-12 text-gray-700 max-w-screen-md mx-auto text-lg leading-relaxed">
                <Markdown>{article.data[0].attributes.content}</Markdown>
              </article>
            </div>
          </Layout>
        </>
      );
    };
    
    export default Article;
    
    export async function getStaticPaths() {
      const articles = await fetchAPI("/articles?populate=*");
    
      return {
        paths: articles.data.map((article) => ({
          params: {
            slug: article.attributes.slug,
          },
        })),
        fallback: false,
      };
    }
    
    export async function getStaticProps({ params, preview = null }) {
      // const article = await fetchAPI(`/articles/?filters\[slug\][$eq]=${params.slug}&populate=*`);
      const article = await getPageData(params.slug, preview);
    
      return {
        props: { article, preview },
        revalidate: 1,
      };
    }
```

The blog post pages should look like this now when the preview mode is activated.

![Article in Preview Mode](https://paper-attachments.dropbox.com/s_89144B6AD52CD00DC4CC5E59EA41A298AA2FE3B2970B9F6B291240BCEA078C30_1659711466976_article-in-preview-mode.png)

Click turn off to deactivate the preview mode. You can also deactivate the preview mode by navigating to `http://localhost:3000/api/exit-preview`.

## Conclusion
The preview mode is an essential static site generator tool that can improve the content editor experience when using the Jamstack architecture. To learn more about Next.js previews, check out the [Next.js Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode) documentation.

[View the source code for this tutorial here.](https://github.com/syboxdigital/strapi-nextjs-preview-blog)