Page builder with Netlify CMS and Next.js

Page builder with Netlify CMS and Next.js

Static websites are known as a combination of html, css and javascript without the need of a backend and database. A static website has already deployed all the files and they are ready for the user without runtime manipulation of the files and the data. Static websites are known to be very fast because the files are ready beforehand. You can read more here about an article we did to explain the difference between static sites vs database powered ones.

When working with different clients, sometimes you have to provide that level of customizability so the client can easily and more conveniently do changes to a dynamic site. For simple websites we often reach out to Netlify CMS in order to let the client control the content of the website and this is how you can built your dynamic page builder with it. For this tutorial we are going to use Next.js, because we want to make use of all that goodness provided by the framework, like file based routing and SEO advantages when deploying to production.

First off lets start by creating a new Next.js project with the following command:

npx create-next-app demo-netlify

After the installation has completed we also need the following packages for Netlify CMS:

npm install netlify-cms-app

After this we can run our dev server with the following command npm run dev

Now on the root folder we create a new folder called cms where we create a config.js file for storing the Netlify CMS configuration. Netlify CMS also offers the ability to make a yaml config file but we find the JavaScript way more convenient for our development.

Our config file should contain the following settings for now:

module.exports = {
  
  // We want to manually init the config file
  cms_manual_init: true,

  // Backend configuration, in this case with git
  backend: {
    name: "git-gateway",
    branch: "master",
    squash_merges: true,
  },

  // Local backend is used during development
  local_backend: true,

  // Where to store the images
  media_folder: "public/images/",

  // Where to link the images
  public_folder: "public/images/",

  // The Pages collection
  collections: [
    {
      name: "Pages",
      label: "Page",
      editor: { preview: false },
      label_singular: "Page",
      folder: "content/pages",
      create: true,
      slug: "{{slug}}",
      extension: "md",
      format: "yaml-frontmatter",
      fields: [
        {
          label: "Title",
          name: "title",
          widget: "string",
          required: true
        },
      ],
    },
  ],
};

As explained with the comments, here are some default config values needed for development which are a bit self explanatory. For our setup we only need one collection called pages. We specify where we want to save the md files, at the following folder: "content/pages". For now we only have one field, named Title. We will come back to this file later on.

Now we need to go to our pages folder and create an admin.js file.

import dynamic from "next/dynamic";
import Head from "next/head";

import config from "../cms/config";

const CMS = dynamic(
  () =>
    import("netlify-cms-app").then((cms) => {
      cms.init({ config });
    }),
  {
    ssr: false,
    loading: () => <h1>Loading</h1>,
  }
);

const AdminPage = () => {
  return (
    <>
      <Head>
        <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
      </Head>
      
      <CMS />
    </>
  );
};

export default AdminPage;

Here we make use of the next dynamic component to only load this page(JavaScript) on demand and only when its needed. This helps us to better optimize our app.

Inside the closure we import the netlify-cms-app that we installed earlier and then we init the config that we also created a bit earlier.

In order to test if everything is working we need to use a beta feature from Netlify CMS to spin up a proxy server for our backend in order to test locally so we don't have to test directly with a live repo. You should run this command in a new tab/window in your cmd so you also have the next dev running.

npx netlify-cms-proxy-server

If everything is right you should visit http://localhost:3000/admin and you should be able to view the Netlify CMS page which looks something like this:

Now that our setup is ready we need to get back to the config file so we can add our dynamic builder to the pages collection.

In order to save some space I will only be showing the content of the collection key, which should be in your config.js file.

collections: [
    {
      name: "Pages",
      label: "Page",
      editor: { preview: false },
      label_singular: "Page",
      folder: "content/pages",
      create: true,
      slug: "{{slug}}",
      extension: "md",
      format: "yaml-frontmatter",
      fields: [
        {
          label: "Title",
          name: "title",
          widget: "string",
          required: true,
        },
        {
          label: "Builder",
          name: "builder",
          widget: "list",
          types: [
            {
              label: "Header Image",
              name: "header",
              widget: "object",
              fields: [
                {
                  label: "Title",
                  name: "title",
                  widget: "string",
                  required: true,
                },
                {
                  label: "Background Image",
                  name: "photo",
                  widget: "image",
                  required: true,
                  media_library: { config: { multiple: false } },
                },
              ],
            },
            {
              label: "CTA Section",
              name: "cta",
              widget: "object",
              fields: [
                {
                  label: "Title",
                  name: "title",
                  widget: "string",
                  required: true,
                },
                {
                  label: "Link",
                  name: "link",
                  widget: "string",
                },
              ],
            },
            {
              label: "Content",
              name: "content",
              widget: "object",
              fields: [
                {
                  name: "Content",
                  widget: "markdown",
                  required: true,
                },
              ],
            },
          ],
        },
      ],
    },
  ],

For our dynamic content to work we need to utilize again a beta feature of Netlify CMS called Variable List Types. This allows us to define a list of types to build our page builder. So in our case we only have 3 types, which are: Header Image, CTA Section and Content. You can add as much as you want from these, or order them in any way you like just like with any other traditional page builder. For this example I went with minimal configuration, but you can add more complex widget types which you can check on the Netlify CMS documentation.

If you did everything right, when you click add new Page you should have a Builder section where you can add different sections to your home page.

The CMS is ready and we can save this, which will trigger the Netlify CMS to create a new file called home under content/pages/home.md as we defined it in our configuration file.

Now that we have the content, we need to parse the md file so we can use it in our frontend. To parse our markdown we will use gray-matter and we can install it with the following command:

npm install --save gray-matter

To load content to our index.js we will make use of the Next.js function called getStaticProps which will pass down as prop to the md file and we will parse that with gray-matter.

Before that, we will create a new component under components/Builder.js so we can map each type from the CMS to a React Component.

function BackgroundImage({ item }) {
  return (
    <div style={{ backgroundImage: `url('${item.photo}')`, height: "200px" }}>
      <h1 style={{ color: "white" }}>{item.title}</h1>
    </div>
  );
}

function Content({ item }) {
  return <div>{item.content}</div>;
}

function Cta({ item }) {
  return <a href={item.link}>{item.title}</a>;
}

const components = {
  header: BackgroundImage,
  content: Content,
  cta: Cta,
};

export default function Builder(props) {
  const Component = components[props.type];

  return <Component item={props.item} />;
}

What we have here is three functional components in our Builder component for each of our types that we did previously in Netlify CMS. We have a component for the Header Image which I have called BackgroundImage, one for Content and the other one for Cta.

After we setup our Builder component, we can now call this in our index.js file.

import fs from "fs";
import { join } from "path";
import matter from "gray-matter";
import Builder from "../components/Builder";

export default function Home({ home }) {
  return home.builder.map((item, index) => {
    return <Builder key={index} type={item.type} item={item} />;
  });
}

function getBySlug(dir, slug) {
  const realSlug = slug.replace(/\.md$/, "");
  const fullPath = join(dir, `${realSlug}.md`);
  const fileContents = fs.readFileSync(fullPath, "utf8");
  const { data } = matter(fileContents);

  return data;
}

export async function getStaticProps() {
  const home = getBySlug("content/pages", "home");

  return {
    props: {
      home,
    },
  };
}

As mentioned earlier, we used the Next.js function called getStaticProps to load the data of home as a prop to the component and we also made a small helper function called getBySlug so we can fetch only the file needed for the homepage.

After we pass it down to the component as prop, we need to map over our builder which is an array of sets that we defined in our CMS, and we pass it down to our Builder component. If you have everything right you should see the homepage populated with the data from the CMS.

This is just the basic version of what can be archived with Netlify CMS as a backend, and Next.js for the frontend. From this you can extend to adding more features which we will list below:

  • Add dynamic routing for the other pages other than home
  • Add more components to the builder
  • Refactor Builder to be used with Next.js dynamic import

The repo for this small demo can be found on our Github repo.