How to Handle Previews in a Headless Architecture

Strapi - Aug 30 '22 - - Dev Community

In this tutorial, you’ll learn to implement a preview system when working with a headless CMS like Strapi.

Author: Mark Munyaka

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.

Prerequisites

Here are what you’ll need to get started:

  • Node.js: This tutorial uses Node v16.1.x
  • Strapi: This tutorial uses Strapi v4.2.x
  • Next.js: This tutorial uses Next.js v12.2.x

Setting up the Project

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

In the strapi-nextjs-previews directory, you’ll install both Strapi and Next.js projects.

Setting up Strapi

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

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:

Administrative user signup form for Strapi

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.

List of articles in Strapi CMS

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:

Content Manager edit view in Strapi CMS

Click the Unpublish button at the top of the screen to change the article into a draft.

A draft entry in Strapi CMS

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.

Setting up Next.js

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

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

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:

First view of a Next.js website

Setting up the UI Framework

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

Once the installation is complete, open the next.config.js file and add the following code:

    const path = require('path');

    module.exports = {
      reactStrictMode: true,
      sassOptions: {
        includePaths: [path.join(__dirname, 'styles')],
      },
    };
Enter fullscreen mode Exit fullscreen mode

This tells the node-sass to compile the SCSS to CSS from the styles directory.
In the styles directory:

  1. Rename the globals.css to globals.scss.
  2. Replace the existing styles in globals.scss with the following line of code:
    @import '/node_modules/bootstrap/scss/bootstrap.scss';
Enter fullscreen mode Exit fullscreen mode

In the pages directory, open the _app.js file and replace the existing code with the following:

    import '/styles/globals.scss';

    function MyApp({ Component, pageProps }) {
      return <Component {...pageProps} />;
    }

    export default MyApp;
Enter fullscreen mode Exit fullscreen mode

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.

Setting up the Markdown to HTML Converter

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

Designing Articles Page

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

In the articles directory, create an index.js file and add the following code:

    import { Container, Row, Col } from 'react-bootstrap';
    import { fetchArticlesApi } from '/lib/articles';
    import Link from 'next/link';

    const ArticlesView = (props) => {
      // 2
      const { articles } = props;
      // 3
      return (
        <section className="py-5">
          <Container>
            <Row>
              <Col lg="7" className="mx-lg-auto">
                <h1 className="mb-5 border-bottom">Articles</h1>
                {articles.data.map((article) => {
                  return (
                    <div key={article.id} className="mb-4">
                      <h2 className="h5">{article.attributes.title}</h2>
                      <p className="mb-2">{article.attributes.description}</p>
                      <Link href={'/articles/' + article.attributes.slug}>
                        <a className="">Read More</a>
                      </Link>
                    </div>
                  );
                })}
              </Col>
            </Row>
          </Container>
        </section>
      );
    };

    export async function getStaticProps() {
      // 1
      const articles = await fetchArticlesApi();
      return {
        props: { articles },
      };
    }

    export default ArticlesView;
Enter fullscreen mode Exit fullscreen mode

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

In the lib directory, create an articles.js file and add the following code:

    const STRAPI_URL = process.env.STRAPI_URL;

    // Helper function to GET the articles from Strapi
    async function fetchArticlesApi() {
      const requestUrl = `${STRAPI_URL}/articles`;
      const response = await fetch(requestUrl);
      return await response.json();
    }

    // Helper function to GET a single article from Strapi
    async function fetchArticleApi(slug) {
      const requestUrl = `${STRAPI_URL}/articles/?filters\[slug\][$eq]=${slug}`;
      const response = await fetch(requestUrl);
      return await response.json();
    }

    // Helper function to GET a single article from Strapi which is in draft state
    async function fetchArticlePreviewApi(slug) {
      const requestUrl = `${STRAPI_URL}/articles?publicationState=preview&filters\[slug\][$eq]=${slug}`;
      const response = await fetch(requestUrl);
      return (await response.json())[0];
    }

    export { fetchArticlesApi, fetchArticleApi, fetchArticlePreviewApi };
Enter fullscreen mode Exit fullscreen mode

In the above code, you have declared two functions:

  1. The fetchArticlesApi function is used to fetch the published articles from the Strapi articles’ endpoint, localhost:1337/api/articles.
  2. The 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.
  3. The 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:

    STRAPI_URL=http://localhost:1337/api
Enter fullscreen mode Exit fullscreen mode

Save your progress and start your Next.js development server by running:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit localhost:3000/articles and you’ll see your articles page rendered by Next.js:

List of articles

Although you have six articles in Strapi CMS, you’ll only see five since one article is in draft state.

Designing a Single Article Page

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:

    import MarkdownView from 'react-showdown';
    import { Container, Row, Col } from 'react-bootstrap';
    import { fetchArticlesApi, fetchArticleApi } from '/lib/articles';

    const ArticleView = (props) => {
      const { article } = props;
      return (
        <section className="py-5">
          <Container>
            <Row>
              <Col lg="7" className="mx-lg-auto">
                <h1>{article.data[0].attributes.title}</h1>
                <MarkdownView markdown={article.data[0].attributes.content} />
              </Col>
            </Row>
          </Container>
        </section>
      );
    };

    // 1
    export async function getStaticPaths() {
      const articles = await fetchArticlesApi();
      const paths = articles.data.map((article) => ({ params: { slug: article.attributes.slug } }));
      return {
        paths: paths,
        fallback: false,
      };
    }

    export async function getStaticProps(context) {
      // 2
      const slug = context.params.slug;
      if (!slug) {
        throw new Error('Slug not valid');
      }

      // 3
      const article = await fetchArticleApi(slug);
      if (!article) {
        return { notFound: true };
      }

      // 4
      return { props: { article } };
    }

    export default ArticleView;
Enter fullscreen mode Exit fullscreen mode

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:

Single article

Enabling the Preview Mode

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:

    CLIENT_PREVIEW_SECRET=<long random string>
Enter fullscreen mode Exit fullscreen mode

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:

    import { fetchArticlePreviewApi } from '/lib/articles';

    export default async function handler(req, res) {
      // Get the preview secret and the slug which needs to be previewed
      const secret = req.query.secret;
      const slug = req.query.slug;
      // If the secret passed as URL query parameter doesn't match the preview secret in .env
      // then send a 401-Unauthorized response
      if (secret !== process.env.CLIENT_PREVIEW_SECRET) {
        return res.status(401).json({ message: 'Invalid token' });
      }

      // If slug is not provided, send a 400-Bad Request response
      if (!slug) {
        return res.status(400).json({ message: 'Parameter `slug` is not provided' });
      }

      // Send a request to Strapi and fetch the article
      // to check if the provided slug exists
      const article = await fetchArticlePreviewApi(slug);

      // If the article is not found, send a 404-Not Found response
      if (article === null) {
        return res.status(404).json({ message: 'Article not found' });
      }

      // Enable Preview Mode by setting the cookies
      res.setPreviewData({});

      // Redirect to the path of the article slug
      res.redirect(307, `/articles/${article.data[0].attributes.slug}`);
    }
Enter fullscreen mode Exit fullscreen mode

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:

    export default function handler(req, res) {
      // Clear the cookies set by the preview mode.
      res.clearPreviewData();

      // Send a 200-Success response to the frontend
      res.status(200).end();
    }
Enter fullscreen mode Exit fullscreen mode

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:

    import Router from 'next/router';
    import MarkdownView from 'react-showdown';
    import { Container, Row, Col } from 'react-bootstrap';
    import { fetchArticlesApi, fetchArticleApi, fetchArticlePreviewApi } from '/lib/articles';

    const ArticleView = (props) => {
      // 4
      const { article, previewMode } = props
      return (
        <section className="py-5">
          <Container>
            <Row>
              <Col lg="7" className="mx-lg-auto">
                {/* 5 */}
                {previewMode ? (
                  <div className="small text-muted border-bottom mb-3">
                    <span>You are currently viewing in Preview Mode. </span>
                    <a role="button" className="text-primary" onClick={() => exitPreviewMode()}>
                      Turn Off Preview Mode
                    </a>
                  </div>
                ) : (
                  ''
                )}
                <h1>{article.data[0].attributes.title}</h1>
                <MarkdownView markdown={article.data[0].attributes.content} />
              </Col>
            </Row>
          </Container>
        </section>
      );
    };
    // 6

    async function exitPreviewMode() {
      const response = await fetch('/api/exit-preview');
      if (response) {
        Router.reload(window.location.pathname);
      }
    }

    export async function getStaticPaths() {
      const articles = await fetchArticlesApi();
      const paths = articles.data.map((article) => ({ params: { slug: article.attributes.slug } }));
      return {
        paths: paths,
        fallback: false,
      };
    }

    export async function getStaticProps(context) {
      const slug = context.params.slug;
      if (!slug) {
        throw new Error('Article not found');
      }

      // 1
      const previewMode = context.preview ? true : null;

      // 2
      let article;
      if (previewMode) {
        article = await fetchArticlePreviewApi(slug);
      } else {
        article = await fetchArticleApi(slug);
      }

      if (!article) {
        return { notFound: true };
      }

      return {
        props: {
          article,
          previewMode, // 3
        }
      };
    }

    export default ArticleView;
Enter fullscreen mode Exit fullscreen mode

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.

Accessing Preview Mode

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=&slug= by replacing:

  • <secret> with the preview secret defined in the .env file
  • <slug> with the slug of a draft article

If you have passed the correct secret and slug query parameters, you will be redirected to http://localhost:3000/articles/:

A draft article in preview mode in Next.js

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:

List of cookies set by the Next.js preview mode API

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:

404 not found page in Next.js

Adding Preview Button in Strapi

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
Enter fullscreen mode Exit fullscreen mode
  1. Choose "plugin" from the list, press Enter, and give the plugin a name in kebab-case. Name it preview-button.
  2. Choose JavaScript for the plugin language.

Make a PreviewLink directory for your preview-button plugin.

    mkdir -p ./src/plugins/preview-button/admin/src/components/PreviewLink
Enter fullscreen mode Exit fullscreen mode

Create an index.js file in ./src/plugins/preview-button/admin/src/components/PreviewLink/ to provide a link for the Preview button.

    // ./src/plugins/preview-button/admin/src/components/PreviewLink/index.js
    import React from 'react';
    import { useCMEditViewDataManager } from '@strapi/helper-plugin';
    import Eye from '@strapi/icons/Eye';
    import { LinkButton } from '@strapi/design-system/LinkButton';

    const PreviewLink = () => {
      const {initialData} = useCMEditViewDataManager();
      if (!initialData.slug) {
        return null;
      }

      return (
        <LinkButton
          size="S"
          startIcon={<Eye/>}
          style=
          href={`${CLIENT_FRONTEND_URL}?secret=${CLIENT_PREVIEW_SECRET}&slug=${initialData.slug}`}
          variant="secondary"
          target="_blank"
          rel="noopener noreferrer"
          title="page preview"
        >Preview
        </LinkButton>
      );
    };

    export default PreviewLink;
Enter fullscreen mode Exit fullscreen mode

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:

    CLIENT_PREVIEW_SECRET=<preview-secret>
    CLIENT_FRONTEND_URL=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

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.

    // ./src/admin/webpack.config.js
    'use strict';
    /* eslint-disable no-unused-vars */
    module.exports = (config, webpack) => {
      // Note: we provide webpack above so you should not `require` it
      // Perform customizations to webpack config
      // Important: return the modified config
      config.plugins.push(
        new webpack.DefinePlugin({
          CLIENT_FRONTEND_URL: JSON.stringify(process.env.CLIENT_FRONTEND_URL),
          CLIENT_PREVIEW_SECRET: JSON.stringify(process.env.CLIENT_PREVIEW_SECRET),
        })
      )
      return config;
    };
Enter fullscreen mode Exit fullscreen mode

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:

    // ./src/plugins/preview-button/admin/src/index.js
    import { prefixPluginTranslations } from '@strapi/helper-plugin';
    import pluginPkg from '../../package.json';
    import pluginId from './pluginId';
    import Initializer from './components/Initializer';
    import PreviewLink from './components/PreviewLink';
    import PluginIcon from './components/PluginIcon';

    const name = pluginPkg.strapi.name;
    export default {
      register(app) {
        app.addMenuLink({
          to: `/plugins/${pluginId}`,
          icon: PluginIcon,
          intlLabel: {
            id: `${pluginId}.plugin.name`,
            defaultMessage: name,
          },
          Component: async () => {
            const component = await import(/* webpackChunkName: "[request]" */ './pages/App');
            return component;
          },

          permissions: [
            // Uncomment to set the permissions of the plugin here
            // {
            //   action: '', // the action name should be plugin::plugin-name.actionType
            //   subject: null,
            // },
          ],
        });

        app.registerPlugin({
          id: pluginId,
          initializer: Initializer,
          isReady: false,
          name,
        });
      },

      bootstrap(app) {
        app.injectContentManagerComponent('editView', 'right-links', {
          name: 'preview-link',
          Component: PreviewLink
        });
      },

      async registerTrads({ locales }) {
        const importedTrads = await Promise.all(
          locales.map(locale => {
            return import(
              /* webpackChunkName: "translation-[request]" */ `./translations/${locale}.json`
            )

              .then(({ default: data }) => {
                return {
                  data: prefixPluginTranslations(data, pluginId),
                  locale,
                };
              })

              .catch(() => {
                return {
                  data: {},
                  locale,
                };
              });
          })
        );
        return Promise.resolve(importedTrads);
      },
    };
Enter fullscreen mode Exit fullscreen mode

The above code injects the PreviewLink component into the content manager.

Next enable the plugin by creating ./config/plugins.js.

    // ./config/plugins.js
    module.exports = {
        // ...
        'Preview Button': {
          enabled: true,
          resolve: './src/plugins/preview-button' // path to plugin folder
        },
        // ...
      }
Enter fullscreen mode Exit fullscreen mode

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

Once the rebuild is complete, start Strapi’s development server by running the following command:

    npm run develop
Enter fullscreen mode Exit fullscreen mode

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:

Preview button in Strapi 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.

Remove highlighted text and click **Save**

Click the Preview button. You’ll be directed to the single article page on your Next.js website.

Updated draft article

Conclusion

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.

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