ACF's Flexible Content with Gatsby & Netlify CMS

Tips & Tricks

ACF's Flexible Content with Gatsby & Netlify CMS

Wojciech Kałużny
Wojciech Kałużny
19 min read

If you’re a WordPress developer then you must have heard about a plugin called Advanced Custom Fields and a Flexible Content field that allows editors to generate new pages easily.

When I started to move more into JAMStack I wanted to recreate ACF’s Flexible Content field in Gatsby. It's possible to use WordPress as a headless CMS and some headless CMS have implemented some sort of an alternative. Prismic has Slices (unfortunately you can’t create multiple repeatable fields within fields).

For smaller projects WordPress or Prismic may be too complex. In such cases, I usually go with my favorite flat-file CMS - Netlify CMS.

Netlify CMS offers everything you need, it’s open-source and free to use. The only thing missing? Flexible Content field. Fortunately, with beta features - Manual Initialization and Variable Types for List fields we can easily create a solution that copies ACF's Flexible Content.

Why using flexible content is a great idea?

Advanced Custom Fields' Flexible Content allows editors to quickly make significant changes without engaging developers. Creating new pages is a breeze, and optimizing for conversions is easier.

Using a singular template may not be the best way to organize your content, especially if you want to quickly test new changes. That's why component-based, modular design gives you much more flexibility.

It lowers development and maintenance costs. Websites are tools that have to generate business value. The better system you build the longer it’ll last without any code changes.

Flexible Content with Netlify CMS - configuration

Code examples assume usage of Manual Initialization, check out this article on DRY Netlify CMS configuration to learn more about that.

I strongly encourage using that instead of standard configuration for optimal developer experience. Manual Initialization leverages javascript files instead of YAML, which makes it easier to maintain in the long run.

You can check out my Gatsby started called Henlo to check out an example of that configuration, and use it as a starter point.

For each Flexible Content item (I’ll call them sections in the article) we need 2 files. A JSX file to render the section (I tend to place them ‘./src/Sections’ folder) and a configuration file for the CMS (‘./src/cms/sections/‘ folder).

Prepare Netlify CMS configuration

First, we'll have to set up a configuration for the collection that we'll use to create pages with sections.

import seo from '@/cms/partials/seo'
import hero from '@/cms/sections/hero'
...

const services = {
  name: 'pages',
  label: 'Pages',
  editor: {
    preview: false,
  },
  folder: 'content/pages',
  slug: '{{slug}}',
  create: true,
  fields: [
	  {
		  label: 'Layout'
		  name: 'layout'
		  widget: 'hidden',
		  default: 'page',
	  }
    {
      label: 'Title',
      name: 'title',
      widget: 'string',
      required: true,
    },
    {
      label: 'Slug',
      name: 'slug',
      widget: 'string',
      required: true,
    },
    {
      label: 'Sections',
      name: 'sections',
      widget: 'list',
      types: [
        hero,
        ...
      ],
    },
    seo,
  ],
}

export default services

In this example, I'm using a javascript file to generate a collection for Netlify CMS. Check this article about Netlify CMS configuration to learn why it's better than YAML files.

The most important field to use is layout. I use them as a way to pass the name of the template file used to render that content type.

As you can see I've added 2 partials - SEO partial that handles SEO content and Hero section. Separating these fields into different files makes it easier to maintain components and reuse them across the project.

Here's the example configuration of the Hero section.

const hero = {
  label: 'Hero',
  name: 'hero',
  widget: 'object',
  collapsed: false,
  fields: [
    {
      label: 'Title',
      name: 'title',
      widget: 'string',
      required: false,
    },
    {
      label: 'Subtitle',
      name: 'subtitle',
      widget: 'string',
      required: false,
    },
    {
      label: 'Content',
      name: 'content',
      widget: 'markdown',
      required: false,
    },
  ],
}

export default hero

Now that we have an initial configuration for Netlify CMS we can go into generating pages from our collection.

Generating pages using Flexible Content with Gatsby & Netlify

Another good practice to follow is to utilize a parent component that manages our sections. This way you can add sections to other templates or pages.

Create SectionsGenerator component

The idea for the component is pretty simple. I was inspired by a recent project I worked on with Prismic, this component is modeled after SliceZone component.

Adding a new section is as easy as importing and matching components to section type (name of the object in Netlify CMS config).

import React from 'react'
import { graphql } from 'gatsby'

import Hero from '@/Sections/Hero'

export default function SectionsGenerator({ sections }) {
	const sectionsComponents = {
		hero: Hero
	}
	
	const sectionsContent = sections.map((section, key) => {
		const Section = sectionsComponents[section.type]
		if (Section) {
			return <Section key={key} data={section} />
		}
		return <div>{section.type}</div>
	})
	
	return (
		<>
			{sectionsContent}
		</>
	)
}

export const query = graphql`
  fragment Sections on MarkdownRemarkFrontmatter {
    sections {
      id
      type
      title
      subtitle
      content
  }
}
`

Additionally, I recommend creating a graphql fragment within the same file. With a single import, we can pass data to query and render sections to any template page in the project (see in next code example)

Prepare a template to render pages

Our template has to do 1 thing - fetch sections data and pass them as a prop to the SectionsGenerator component.

With this approach, it's also possible to use a single layout for every page. Using the useStaticQuery hook it's possible to add additional data to each section.

import React from 'react'
import { graphql } from 'gatsby'

import Layout from '@/components/Layout'
import SectionsGenerator from '@/components/SectionsGenerator'

import SEO from '@/components/SEO/Seo'

const SectionPageTemplate = ({ data }) => {
  const sections = data.frontmatter.sections
  return (
    <>
      <SectionsGenerator sections={sections} />
    </>
  )
}

const LandingPage = ({ data }) => {
  return (
    <Layout hideNav={true}>
      <SEO data={data.page.frontmatter.seo} />
      <SectionPageTemplate data={data.page} />
    </Layout>
  )
}

export default SectionPage

export const sectionsPageQuery = graphql`
  query SectionPage($id: String!) {
    page: markdownRemark(id: { eq: $id }) {
      id
      fields {
        slug
      }
      html
      frontmatter {
        title
        ...Sections
        ...SEO
      }
    }
  }
`

By writing a fragment our query stays extremely short regardless of the number of sections the project supports.

Configure Gatsby-node to work with Netlify CMS with Flexible Content

With all of the components ready we can proceed to the gatsby-node configuration.

exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions

  return graphql(`
    {
      allMarkdownRemark(limit: 1000) {
        edges {
          node {
            id
            fields {
              slug
            }
            frontmatter {
              layout
              title
              slug
            }
          }
        }
      }
    }
  `).then((result) => {
    if (result.errors) {
      result.errors.forEach((e) => console.error(e.toString()))
      // return Promise.reject(result.errors);
    }

    // Filter out the footer, navbar, and meetups so we don't create pages for those
    const postOrPage = result.data.allMarkdownRemark.edges.filter((edge) => {
	    let layout = edge.node.frontmatter.layout
	    return layout == null || layout == 'hidden'
    })

    postOrPage.forEach((edge) => {
      const id = edge.node.id
      let component = path.resolve(
        `src/templates/${String(edge.node.frontmatter.layout)}.js`,
      )
      if (fs.existsSync(component)) {
        switch (edge.node.frontmatter.layout) {
          case 'page':
            createPage({
              path: `/${Helper.slugify(edge.node.frontmatter.slug)}/`,
              component,
              context: {
                id,
              },
            })
            break
            ...
        }
      }
    })
  })
}

To generate correct slugs we have to leverage the slug field added to each page in the collection. This way editors can update the URLs to create lots of pages, even with hierarchy (although it won't be reflected in the UI of Netlify CMS).

In my project I tend to use a trailing slash in URLs, this helps to avoid some SEO optimization issues with Gatsby and Netlify.

I'm using a helper to clean up pages' URLs and make sure it won't cause any issues.

Watch out for these issues

There’s one problem if we try to create a page and call a non-existent element page generation will fail. Why?

Gatsby infers the type of field based on the content we provide. If content doesn't exist for that field the build process fails. To avoid that issue we have to let Gatsby know what to expect.

We do that but defining types in the gatsby-node.js file. Here's an example that I wrote when working on new landing pages for Clean Commit's website.

exports.createSchemaCustomization = ({ actions }) => {
  actions.createTypes(`
    type Button {
      text: String
      link: String
    }

    type List {
      title: String
      content: String
    }

    type Form {
      provider: String
      title: String
      formid: Int
      redirect: String
      button: String
    }

    type FAQ {
      question: String
      answer: String
    }

    type MarkdownRemarkFrontmatterSections @infer {
      id: String
      type: String
      subheader: String
      title: String
      subtitle: String
      background: String
      content: String
      variant: String
      video: String
      bulletpoints: [String]
      secondarycontent: String
      button: Button
      list: [List]
      form: Form
      faqs: [FAQ]
    }
  `)
}

We've prepared 17 different sections that our team can use to create new landing pages and services. With defined types, we can safely deploy the website without any issues during the build process.

Sections fields naming

Creating a Flexible Content experience with Netlify CMS is different than in any other Headless CMS. At this point, there's no way to query content only for one section. That's why the naming convention for fields has to be consistent (or you'll spend a lot of time writing custom types definition).

That's why it's important to reuse the same names and be as consistent as possible across multiple sections. With Clean Commit's landing pages almost every section uses title, content, subheader, and button fields. So keep that in mind when working on your project!

If you want to check out how this solution is working and what you can create take a look at Clean Commit' service pages like website development, app development or front-end development.

Along with my team we created (and actively maintaining) our own Gatsby Starter for Netlify CMS called Henlo - check it out and show us some love!

How to create Flexible Content field in Netlify CMS

  • Use manual initialization to make configuration file management easier.
  • Leverage List widget and use types option
  • Create a component that will render each section component
  • Add that component to templates where you want to use it
  • Define types of fields used in sections to avoid build errors with type inference in Gatsby

If you enjoyed this article, consider buying me a coffee ;)

Buy Me A Coffee

Contact me today!
Let's work on your project

There's no point in wasting time, let's talk about your project today. I'm certain I'll be able to help you enable your website to get more leads.

Take me to contact!
Close cookies notice

By closing this message or clicking Accept, you consent to our cookies on this device in accordance with the cookie policy (available here).

Accept
Decline