How to Create Pages on the fly with Dynamic Zone

Shada - Aug 17 '21 - - Dev Community

Before I start, I have to make sure you are the right person to read what will follow.

https://media.giphy.com/media/fnuSiwXMTV3zmYDf6k/giphy.gif

This article will explain in details how you can create pages on the fly with Strapi using Dynamic Zones for a Next.js website. It is therefore intended for developers as it will be quite technical.

However, if you are a member of a marketing team and you are curious to learn more on the topic with less technical terms, I redirect you to an article which is about how our own marketing team uses Strapi.

Let's get started!

Strapi

  • Create a Strapi project using an SQLite3 database by running the following command:
npx create-strapi-app api --quickstart
Enter fullscreen mode Exit fullscreen mode
  • Create your admin user and let's meet in the Content-Types Builder!
  • The first thing you want to do is to create a collection-type with localization enabled that will represent a page and contains these fields.

Capture_decran_2021-07-21_a_09.21.54.png

Capture_decran_2021-07-21_a_09.18.11.png

  • title (text, short)
  • slug (text, short)
  • seo component:
    • metaTitle (text, short)
    • metaDescription (text, long)
    • meta repeatable component:
      • name (text, short)
      • content (text, short)
    • preventIndexing (boolean)
    • structuredData (JSON)
    • metaImage (single media)
  • block (dynamic zone)
  • publish_at (datetime)

Capture_decran_2021-07-21_a_09.42.21.png

You can decide to enable the localization or not on some fields in your collection-types by editing them:

Capture_decran_2021-07-21_a_09.23.32.png

  • You can press save! Here are some explanation before going further:

  • title: Name of your page (localized)

  • slug: Slug of your page (not localized)

  • seo component: Contains all your meta tags for SEO.

  • block: Dynamic zone that will allow you to dynamically include components on your page.

  • publish_at: Field that will allow you to schedule the publication of a page.

I am ready to bet that the only difficulty you can have right now is about the famous block Dynamic Zone. I'm going to quickly explain what it will do.

Easy definition: A Dynamic Zone is used for dynamically include components in your Content-Types.

When you include a component in a content-type, it will necessarily be included like your seo component. If you go to your content manager trying to create a new Page, you will see your seo component no matter what.

Capture_decran_2021-07-21_a_09.33.23.png

But what if you want to have a specific component on a page like an FAQ on your homepage but you don't want it on your pricing page? You simply use Dynamic Zone 😉

Here is an example of what you can have. This screenshot comes from our live demo FoodAdvisor that you can try for free.

Capture_decran_2021-07-21_a_09.35.16.png

The Dynamic Zone contains a list of components it can use for a specific content-types. You can then have the possibility to include one of these components in your content-types.

I guess it's time to create this list!

  • Go back to your Content-Types Builder. On the Page collection-type, click on Add a component for your block Dynamic Zone

Capture_decran_2021-07-21_a_09.42.21.png

  • Click on "Create a new component" and fill the modal like this:

Capture_decran_2021-07-21_a_09.45.50.png

The category on the right will allow you to classify your components. I advise you to include all your block components in the "blocks" category. Concerning others components, you can create other categories as you want.

  • Create the fields of this "hero" components to look like the screenshot below:

Capture_decran_2021-07-21_a_09.47.44.png

As you can see this component also contains components. Cool right? But maybe a little bit complicated to conceive. I advise you to include the header, buttons and link components in a shared called category.

If you are having trouble creating these components in the admin, you can manually create them in your code editor:

./components/shared/link.json

{
  "collectionName": "components_shared_links",
  "info": {
    "name": "link",
    "icon": "backward",
    "description": ""
  },
  "options": {},
  "attributes": {
    "href": {
      "type": "string",
      "required": true
    },
    "label": {
      "type": "string",
      "required": true
    },
    "target": {
      "type": "enumeration",
      "enum": [
        "_blank"
      ]
    },
    "isExternal": {
      "type": "boolean",
      "default": false,
      "required": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

./components/shared/button.json

{
  "collectionName": "components_shared_buttons",
  "info": {
    "name": "button",
    "icon": "compress",
    "description": ""
  },
  "options": {},
  "attributes": {
    "theme": {
      "type": "enumeration",
      "enum": [
        "primary",
        "secondary",
        "muted"
      ],
      "default": "primary",
      "required": true
    },
    "link": {
      "type": "component",
      "repeatable": false,
      "component": "shared.link"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

./components/shared/header.json

{
  "collectionName": "components_shared_headers",
  "info": {
    "name": "header",
    "icon": "heading",
    "description": ""
  },
  "options": {},
  "attributes": {
    "theme": {
      "type": "enumeration",
      "enum": [
        "primary",
        "secondary",
        "muted"
      ],
      "default": "primary",
      "required": true
    },
    "label": {
      "type": "string",
      "required": false
    },
    "title": {
      "type": "string",
      "required": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

./components/blocks/hero.json

{
  "collectionName": "components_slices_heroes",
  "info": {
    "name": "hero",
    "icon": "pizza-slice"
  },
  "options": {},
  "attributes": {
    "images": {
      "collection": "file",
      "via": "related",
      "allowedTypes": [
        "images",
        "files",
        "videos"
      ],
      "plugin": "upload",
      "required": false,
      "pluginOptions": {}
    },
    "header": {
      "type": "component",
      "repeatable": false,
      "component": "shared.header"
    },
    "text": {
      "type": "string"
    },
    "buttons": {
      "type": "component",
      "repeatable": true,
      "component": "shared.button"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Perfect! Now you should have this component in your Dynamic Zone!

Capture_decran_2021-07-21_a_10.00.11.png

  • Press save! Now you can create an homepage!
  • Go in the Content Manager and create a new page like the one below:

Important: An homepage doesn't have a slug. It must contains an empty string. To do this, just write something inside the slug field like "hello" then remove it. It will contains an empty string instead of nothing.

As you can see, these screenshots are taken from the application of our live demo which is FoodAdvisor. Feel free to place your own text/images.

Capture_decran_2021-07-21_a_11.31.57.png
Capture_decran_2021-07-21_a_11.32.32.png

We have our three components: header, button and link. This allows you to not re-create these fields inside your blocks component.

  • You can create another localized version of this page if you activated another locale in your application, other than that, press save!
  • Be sure that you activated the find action for the Page collection-type in the USERS & PERMISSIONS PLUGIN to be able to fetch them through the API.
  • Browse this url: http://localhost:1337/pages?slug=

You should have your homepage data in JSON format! Perfect! We are done concerning the Strapi side of this tutorial. To summarize, you:

  • Created a Page collection-type
  • Created a Dynamic Zone for the Page collection-type
  • Included a hero component in this Dynamic Zone

Now it is time to create a Next.js application that will render these pages!

Next.js

Let's get started!

  • Create a Next.js application by running the following command:
npx create-next-app client
Enter fullscreen mode Exit fullscreen mode

The first thing we want to do is to create some useful functions for our application.

  • Create a ./client/utils/index.js file including the following functions:
// Get the url of the Strapi API based om the env variable or the default local one.
export function getStrapiURL(path) {
  return `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337"}${path}`;
}

// This function will get the url of your medias depending on where they are hosted
export function getStrapiMedia(url) {
  if (url == null) {
    return null;
  }
  if (url.startsWith("http") || url.startsWith("//")) {
    return url;
  }
  return `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:1337"}${url}`;
}

// handle the redirection to the homepage if the page we are browsinng doesn't exists
export function redirectToHomepage() {
  return {
    redirect: {
      destination: `/`,
      permanent: false,
    },
  };
}

// This function will build the url to fetch on the Strapi API
export function getData(slug, locale) {
  const slugToReturn = `/${slug}?lang=${locale}`;
  const apiUrl = `/pages?slug=${slug}&_locale=${locale}`;

  return {
    data: getStrapiURL(apiUrl),
    slug: slugToReturn,
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Create a ./client/pages/services/api.js containing the following code:
import delve from "dlv";

// This functionn can merge required data but it is not used here.
export async function checkRequiredData(block) {
  return block;
}

// This function will get the data dependencies for every blocks.
export async function getDataDependencies(json) {
  let blocks = delve(json, "blocks", []);
  blocks = await Promise.all(blocks.map(checkRequiredData));
  return {
    ...json,
    blocks,
  };
}
Enter fullscreen mode Exit fullscreen mode
  • Create a ./client/utils/localize.js file containing the following code:
import delve from "dlv";

// This function simply return the slug and the locale of the request with default values
export function getLocalizedParams(query) {
  const lang = delve(query, "lang");
  const slug = delve(query, "slug");

  return { slug: slug || "", locale: lang || "en" };
}
Enter fullscreen mode Exit fullscreen mode

Now everything should be ready to start on a solid basis.

  • Create a ./client/pages/[[...slug]].jsfile containing the following code:
import delve from "dlv";

import { getDataDependencies } from "./services/api";
import { redirectToHomepage, getData } from "../utils";
import { getLocalizedParams } from "../utils/localize";

const Universals = ({ pageData }) => {
  const blocks = delve(pageData, "blocks");
  return <div></div>;
};

export async function getServerSideProps(context) {
  const { slug, locale } = getLocalizedParams(context.query);

  try {
    const data = getData(slug, locale);
    const res = await fetch(delve(data, "data"));
    const json = await res.json();

    if (!json.length) {
      return redirectToHomepage();
    }

    const pageData = await getDataDependencies(delve(json, "0"));
    console.log(pageData);
    return {
      props: { pageData },
    };
  } catch (error) {
    return redirectToHomepage();
  }
}

export default Universals;
Enter fullscreen mode Exit fullscreen mode
  • Refresh your application in the browser and give a look at your Next.js logs in your terminal. You should see something like this:

Capture_decran_2021-07-21_a_15.58.27.png

Awesome! Now it is time to build the BlockManager! This component will simply tell your page to render this or this component based on witch components the Dynamic Zone includes.

  • Create a ./client/components/shared/BlockManager/index.js file containing the following code:
import Hero from '../../blocks/Hero';

const getBlockComponent = ({ __component, ...rest }, index) => {
  let Block;

  switch (__component) {
    case 'blocks.hero':
      Block = Hero;
      break;
  }

  return Block ? <Block key={`index-${index}`} {...rest} /> : null;
};

const BlockManager = ({ blocks }) => {
  return <div>{blocks.map(getBlockComponent)}</div>;
};

BlockManager.defaultProps = {
  blocks: [],
};

export default BlockManager;
Enter fullscreen mode Exit fullscreen mode

You can see that this component is simply looking at every components included in the Dynamic Zone. Since you only included the hero one, we simply tell this file to render it. However we need to create the hero component file in Next.js

  • Create ./client/components/blocks/Hero/index.js file containing the following code:
import delve from 'dlv';

import ImageCards from './image-cards';
import CustomLink from '../../shared/CustomLink';

const Hero = ({ images, header, text, buttons }) => {
  const title = delve(header, 'title');

  return (
    <section className="text-gray-600 body-font py-40 flex justify-center items-center 2xl:h-screen">
      <div className="container flex md:flex-row flex-col items-center">
        <div className="mt-4 relative relative-20 lg:mt-0 lg:col-start-1">
          <ImageCards images={images} />
        </div>

        <div className="lg:flex-grow md:w-1/2 my-12 lg:pl-24 md:pl-16 md:mx-auto flex flex-col md:items-start md:text-left items-center text-center">
          {title && (
            <h1 className="title-font lg:text-6xl text-5xl mb-4 font-black text-gray-900">
              {title}
            </h1>
          )}

          {text && <p className="mb-8 px-2 leading-relaxed">{text}</p>}

          <div className="block space-y-3 md:flex md:space-y-0 space-x-2">
            {buttons &&
              buttons.map((button, index) => (
                <button
                  key={`heroButton-${index}`}
                  className={`inline-block text-${delve(
                    button,
                    'theme'
                  )}-text bg-${delve(
                    button,
                    'theme'
                  )} border-0 py-2 px-6 focus:outline-none hover:bg-${delve(
                    button,
                    'theme'
                  )}-darker rounded-full shadow-md hover:shadow-md text-lg`}
                >
                  <CustomLink {...delve(button, 'link')} />
                </button>
              ))}
          </div>
        </div>
      </div>
    </section>
  );
};

Hero.defaultProps = {};

export default Hero;
Enter fullscreen mode Exit fullscreen mode

This component simply display the Hero component from your Dynamic Zone. It requires two other components to work.

  • Create a ./client/components/blocks/Hero/image-cards.js file containing the following code:
import delve from 'dlv';

import { getStrapiMedia } from '../../../utils';

const ImageCards = ({ images }) => {
  return (
    <div className="relative space-y-4">
      <div className="flex items-end justify-center lg:justify-start space-x-4">
        {images &&
          images
            .slice(0, 2)
            .map((image, index) => (
              <img
                className="rounded-lg shadow-lg w-32 md:w-56"
                key={`heroImage-${index}`}
                width="200"
                src={getStrapiMedia(delve(image, 'url'))}
                alt={delve(image, 'alternativeText')}
              />
            ))}
      </div>
      <div className="flex items-start justify-center lg:justify-start space-x-4 md:ml-12">
        {images &&
          images
            .slice(2, 4)
            .map((image, index) => (
              <img
                className="rounded-lg shadow-lg w-32 md:w-56"
                key={`heroImage-${index}`}
                width="200"
                src={getStrapiMedia(delve(image, 'url'))}
                alt={delve(image, 'alternativeText')}
              />
            ))}
      </div>
    </div>
  );
};

ImageCards.defaultProps = {};

export default ImageCards;
Enter fullscreen mode Exit fullscreen mode

This will simply display the images. You'll need to have 4 images for your hero components to be correctly displayed.

  • Create ./client/components/shared/CustomLink/index.js file containing the following code:
import Link from 'next/link';

const CustomLink = ({ label, href, locale, target, isExternal }) => {
  if (isExternal) {
    return (
      <Link href={href}>
        <a target={target}>{label}</a>
      </Link>
    );
  } else {
    return (
      <Link href={`${href}?lang=${locale || 'en'}`}>
        <a target={target}>{label}</a>
      </Link>
    );
  }
};

CustomLink.defaultProps = {};

export default CustomLink;
Enter fullscreen mode Exit fullscreen mode

This component allows to handle link that are internal or external which is pretty useful!

  • Refresh your browser and see the results! Tada!

Capture_decran_2021-07-21_a_17.01.30.png

Wait a second! This looks horrible! Can you tell what is missing here?

https://media.giphy.com/media/3o7TKTDn976rzVgky4/giphy.gif

Tailwind CSS of course! It is a CSS framework that will allow you to rapidly build modern websites with a beautiful UI.

  • Execute the following command to install Tailwind CSS on your Next.js application:
yarn add tailwindcss@latest postcss@latest autoprefixer@latest
Enter fullscreen mode Exit fullscreen mode
  • Create a ./client/tailwind.config.js file containing the following code:
module.exports = {
  // mode: 'jit',
  purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: '#e27d60',
          light: '#e48a6f',
          darker: '#cb7056',
          text: '#FFFFFF',
          lightest: '#f0beaf',
        },
        secondary: {
          DEFAULT: '#41b3a3',
          light: '#85dcb',
          darker: '#3aa192',
          text: '#FFFFFF',
          lightest: '#ecf7f5',
        },
        muted: {
          DEFAULT: '#E5E7EB',
          ligth: '#F3F4F6',
          darker: '#D1D5DB',
          text: '#555b66',
        },
      },
    },
  },
  variants: {
    extend: {
      // ...
      ringWidth: ['hover', 'active'],
    },
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

This is the default theme we use for FoodAdvisor, feel free to customize it!

  • Create a ./client/postcss.config.js file containing the following code:
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
Enter fullscreen mode Exit fullscreen mode
  • Update your ./client/pages/_app.js to include your tailwind theme:
// Add this line
import "tailwindcss/tailwind.css";

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

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

You should be good!

  • Refresh your app to see the result!

Capture_decran_2021-07-21_a_17.10.57.png

Great! Now let's add another components to our Dynamic Zone so that we can display it on our website!

  • Create a new CtaCommandLine component in your Dynamic Zone containing these fields:

Capture_decran_2021-07-21_a_17.38.10.png

  • Create the content of this CTA in the Content Manager of your homepage:

Capture_decran_2021-07-21_a_17.40.37.png
Capture_decran_2021-07-21_a_17.39.04.png

Now you simply need to create the Next.js component for this CtaCommandLine and to include it in your BlockManager.

  • Create a ./client/components/blocks/CtaCommandLine/index.js file containing the following:
import { CopyBlock, nord } from 'react-code-blocks';

const CtaCommandLine = ({ title, text, theme, commandLine }) => {
  return (
    <div className={`bg-${theme}`}>
      <div className="text-center w-full mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8 z-20">
        <h2 className={`text-3xl font-extrabold text-black sm:text-4xl`}>
          {title && <span className="block">{title}</span>}
          {text && <span className={`block text-white`}>{text}</span>}
        </h2>
        <div className="py-12 lg:flex-shrink-0 flex items-center justify-center">
          <div className="block md:w-2/5 w-full shadow-2xl text-center">
            <CopyBlock
              text={commandLine}
              language="bash"
              codeBlock
              theme={nord}
              showLineNumbers={false}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

CtaCommandLine.defaultProps = {};

export default CtaCommandLine;
Enter fullscreen mode Exit fullscreen mode

This component requires the react-code-blocks package.

  • Stop your server and install the package with the following command:
yarn add react-code-blocks
Enter fullscreen mode Exit fullscreen mode
  • Start your server again!
  • Update your ./client/components/shared/BlockManager/index.js file to look like this:
import Hero from "../../blocks/Hero";
import CtaCommandLine from "../../blocks/CtaCommandLine";

const getBlockComponent = ({ __component, ...rest }, index) => {
  let Block;

  switch (__component) {
    case "blocks.hero":
      Block = Hero;
      break;
    case "blocks.cta-command-line":
      Block = CtaCommandLine;
      break;
  }

  return Block ? <Block key={`index-${index}`} {...rest} /> : null;
};

const BlockManager = ({ blocks }) => {
  return <div>{blocks.map(getBlockComponent)}</div>;
};

BlockManager.defaultProps = {
  blocks: [],
};

export default BlockManager;
Enter fullscreen mode Exit fullscreen mode

You are simply importing your new component so that your Block Manager can display it!

  • Refresh your app and see the result!

Capture_decran_2021-07-21_a_17.45.41.png

Well that is pretty much it! I hope that you understand the power of the Dynamic Zone feature now! The process here to create sections of a page is very simple and fast to do. In fact, you need to:

Strapi Side

  • Create a Dynamic Zone
  • Include components in your Dynamic Zone (Hero, CTA).
  • Create the data for these components in the Content Manager of the page (Homepage)

Next.js Side

  • Create the component of the component (Hero → ./clients/components/blocks/Hero/index.js )
  • Include this component in the BlockManager component.
  • Include the BlockManager component in your Next.js page ([[...slug]].js)

So the goal for you, when it comes to create a whole website with this architecture, is to clearly define with your marketing team every components you want to be able to display on any pages. You simply define their fields in Strapi, you create their frontend component in Next.js and after that, your marketing team will have the flexibility and freedom to create the content.

It sounds like a conclusion but this tutorial is not over! Let's create another page to prove that you can actually create pages on the fly! Let's build a pricing page!

  • First, let's create a pricing components part of the blocks category to add to our Dynamic Zone that looks like this:

Capture_decran_2021-07-21_a_18.00.27.png

This one can be a little bit tricky if you are not that familiar with components. But this one contains the header component that you already have.

  • Create a perks component part of a pricing category
    • name (short text)
    • included (boolean)
  • Create a pricingCards component part of a pricing category
    • title (short text)
    • description (long text)
    • price (number int)
    • perks repeatable component
  • Then you can click on Add a component in your Dynamic Zone and create your pricing component by adding the header and the pricingCards (repeatable) components.

You are doing great! Keep up the good work!

  • Create a pricing page containing the following fields for now

Capture_decran_2021-07-21_a_17.58.25.png

Again, feel free to use different images or text. This is just an example.

  • Now you can add your new pricing component to your page:

Capture_decran_2021-07-21_a_18.08.01.png

As you saw, the pricingCards and the perks components are repeatable which means that you can add an infinite number of them. The first pricing card is the free one that contains its title, description, price and perks. Again, as the perks is repeatable, you can have a lot of them!

Each perk contains a name and a boolean if this is included in the pricing plan.

Nothing else to do in the admin! Let's dive in Next.js!

  • Create a ./client/components/blocks/Pricing/index.js file containing the following code:
const Pricing = ({ header, pricingCards }) => {
  return (
    <div className="bg-white pb-60">
      <div className="text-center pt-24">
        {header && (
          <h2
            className={`text-${header.theme} font-extrabold tracking-wide uppercase`}
          >
            {header.label}
          </h2>
        )}

        {header && (
          <p className="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
            {header.title}
          </p>
        )}
      </div>

      <div className="sm:flex flex-wrap justify-center items-center text-center gap-8 pb-12 pt-16 mt-4">
        {pricingCards &&
          pricingCards.map((card, index) => (
            <div
              className="shadow-lg rounded-2xl w-64 bg-white dark:bg-gray-800 p-4"
              key={`pricingCard-${index}`}
            >
              <p className="text-gray-800 dark:text-gray-50 text-xl font-medium mb-4">
                {card.title}
              </p>
              <p className="text-gray-900 dark:text-white text-3xl font-bold">
                ${card.price}
                <span className="text-gray-300 text-sm">/ month</span>
              </p>
              <p className="text-gray-600 dark:text-gray-100  text-xs mt-4">
                {card.description}
              </p>
              <ul className="text-sm text-gray-600 dark:text-gray-100 w-full mt-6 mb-6">
                {card.perks &&
                  card.perks.map((perk, index) => (
                    <li
                      className="mb-3 flex items-center"
                      key={`perk-${index}`}
                    >
                      {perk.included ? (
                        <svg
                          className="h-6 w-6 mr-2"
                          xmlns="http://www.w3.org/2000/svg"
                          width="6"
                          height="6"
                          stroke="currentColor"
                          fill="#10b981"
                          viewBox="0 0 1792 1792"
                        >
                          <path d="M1412 734q0-28-18-46l-91-90q-19-19-45-19t-45 19l-408 407-226-226q-19-19-45-19t-45 19l-91 90q-18 18-18 46 0 27 18 45l362 362q19 19 45 19 27 0 46-19l543-543q18-18 18-45zm252 162q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"></path>
                        </svg>
                      ) : (
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          width="6"
                          height="6"
                          className="h-6 w-6 mr-2"
                          fill="red"
                          viewBox="0 0 1792 1792"
                        >
                          <path d="M1277 1122q0-26-19-45l-181-181 181-181q19-19 19-45 0-27-19-46l-90-90q-19-19-46-19-26 0-45 19l-181 181-181-181q-19-19-45-19-27 0-46 19l-90 90q-19 19-19 46 0 26 19 45l181 181-181 181q-19 19-19 45 0 27 19 46l90 90q19 19 46 19 26 0 45-19l181-181 181 181q19 19 45 19 27 0 46-19l90-90q19-19 19-46zm387-226q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"></path>
                        </svg>
                      )}
                      {perk.name}
                    </li>
                  ))}
              </ul>
              <button
                type="button"
                className="py-2 px-4 bg-secondary hover:bg-secondary-darker text-white w-full transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2  rounded-lg "
              >
                Choose plan
              </button>
            </div>
          ))}
      </div>
    </div>
  );
};

Pricing.defaultProps = {};

export default Pricing;
Enter fullscreen mode Exit fullscreen mode
  • Now include this component in your ./client/components/shared/BlockManager/index.js file with the following code:
import Hero from "../../blocks/Hero";
import Pricing from "../../blocks/Pricing";
import CtaCommandLine from "../../blocks/CtaCommandLine";

const getBlockComponent = ({ __component, ...rest }, index) => {
  let Block;

  switch (__component) {
    case "blocks.hero":
      Block = Hero;
      break;
    case "blocks.pricing":
      Block = Pricing;
      break;
    case "blocks.cta-command-line":
      Block = CtaCommandLine;
      break;
  }

  return Block ? <Block key={`index-${index}`} {...rest} /> : null;
};

const BlockManager = ({ blocks }) => {
  return <div>{blocks.map(getBlockComponent)}</div>;
};

BlockManager.defaultProps = {
  blocks: [],
};

export default BlockManager;
Enter fullscreen mode Exit fullscreen mode
  • Go to http://localhost:3000/pricing

Capture_decran_2021-07-21_a_18.13.09.png

Awesome isn't it! Well that's it for this tutorial! I showed you a very simple and quick way to create pages on the fly with Strapi and Next.js. We can totally improve this application by implementing i18n and previews.

This would complexify this tutorial, but I can write a second part if you are interested! In that case, let me know by starting a discussion on our forum!

Capture_decran_2021-07-21_a_18.16.52.png

See you in the next article!

https://media.giphy.com/media/l0NwC1UnHzAHfC50c/giphy.gif

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