How to Implement Previews with Next Applications using a Strapi backend

Strapi - Sep 19 '22 - - Dev Community

Author: Mark Munyaka

In this guide, you will learn about the importance of previews in Jamstack. I will also walk you through the process of adding the previews mode to a blog built with Next.js and Strapi headless CMS 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: 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: Tailwind CSS is a utility-first CSS framework. It enables you to add CSS directly to your HTML for easier development and scaling.

  • Strapi 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: 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 as a starter for faster and easier development.

    npx create-strapi-app backend  --quickstart --template @strapi/template-blog@1.0.0 blog 
Enter fullscreen mode Exit fullscreen mode

View the blog template here.

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

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

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

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

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

Next.js Setup

The next step in this guide will be the frontend setup with Next.js. First, initialize your frontend project with this command.

    npx create-next-app frontend
Enter fullscreen mode Exit fullscreen mode

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.

Next.js frontend homepage

Tailwind CSS Setup

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

    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode

Then, generate tailwind CSS configuration files.

    npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

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.

    /* ./tailwind.config.js */
    /** @type {import('tailwindcss').Config} */ 
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
Enter fullscreen mode Exit fullscreen mode

Complete your Tailwind CSS setup by adding the @tailwind directives for each of Tailwind’s layers to your ./styles/globals.css file.

    /* ./styles/globals.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

For more information on installing Tailwind CSS, refer to the Install Tailwind CSS with Next.js guide.

Markdown-to-jsx Installation

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

    npm install markdown-to-jsx
Enter fullscreen mode Exit fullscreen mode

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:

    /* ./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;
    }
Enter fullscreen mode Exit fullscreen mode

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:

    /* ./components/layout.js */
    import Header from './header';
    import Footer from './footer';

    const Layout = ({ children }) => (
        <>
            <Header />
            {children}
            <Footer />
        </>
    );

    export default Layout;
Enter fullscreen mode Exit fullscreen mode

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:

    /* ./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;
Enter fullscreen mode Exit fullscreen mode

Then, add the following code to the ./components/footer.js file.

    /* ./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;
Enter fullscreen mode Exit fullscreen mode

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:

    /* ./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 };
      }
    }
Enter fullscreen mode Exit fullscreen mode

In this code, we exported articles with the published status from the Strapi backend 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:

    npm run dev`
Enter fullscreen mode Exit fullscreen mode

The homepage on http://localhost:3000 should be similar to this:

Next Blog with Published Articles

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:

    /* ./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,
     };
    }
Enter fullscreen mode Exit fullscreen mode

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:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit one of the published articles by entering Sample Published Articles 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

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=****
Enter fullscreen mode Exit fullscreen mode

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.

    /* ./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;
    }
Enter fullscreen mode Exit fullscreen mode

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:

    /* ./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();
    };
Enter fullscreen mode Exit fullscreen mode

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:

import { fetchAPI, getPageData } from "../../lib/api";
Enter fullscreen mode Exit fullscreen mode

Change your getStaticProps() code as follows:

    /* ./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,
        };
       } 
Enter fullscreen mode Exit fullscreen mode

Accessing the Preview Mode

Access the preview mode by navigating to this url http://localhost:3000/api/preview?secret=&slug=

Where:

= secret token defined in your .env file
= 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:

    /*. /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 })
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    /* ./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,
      };
    }
Enter fullscreen mode Exit fullscreen mode

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

Article in Preview Mode

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 documentation.

View the source code for this tutorial here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .