Dynamic Content Types with Gatsby & Contentful

September 9th, 2020



originally posted at nansen.com

If you've worked with Gatsby, you know there are all kinds of different ways to source content. Everything from markdown files to full fledged CMS have been used. Here at Nansen, our content source of choice is Contentful. Its flexibility allows for us to quickly develop and easily scale for any Gatsby implementation.

One great feature of Contentful is its flexibility when it comes to defining the content model. If you have a content type, you can easily set it up so that it can reference any other content type. This is beneficial if you want to create pages where the content can be dynamically defined in the CMS.

For example, you may have a standard page content type you defined in Contentful. This standard page probably has all the properties you would expect of a web page. Title, slug (path), meta data, hero image, etc. But for the actual content of the page, you might want more flexibility. Maybe you want to add a product that renders as a teaser. Or maybe its a list of FAQs. Content like this is best defined as their own content types in Contentful and referenced, not as properties on the page itself.

I wanted to take you through a couple steps we follow for our implementations. We find this to be the easiest way to develop for this kind of page content. I won't go much into the Contentful side and the content model. But if you want to learn more, I suggest you check out our earlier post on Content Modeling in Contentful.

Query Contentful

In Gatsby, the standard way of making calls for content is by using GraphQL. If we keep using the standard page example, you will have built out a template in code for the standard page. In our examples we'll call it standard-page.js. This template would be referenced from your gatsby-node.js file, where you do your initial Contentful query for all the Standard Page content types, getting some initial info like content id, slug, etc.

On the standard-page.js template itself, you need to make a call for the rest of the content for that specific page. Below, we are getting content for the "About us" page specifically.

Your first instinct might be to build your call like the following, where you specify all your properties, as well as any referenced content. You are also probably using the inline fragments for querying your Contentful content :

query MyQuery { contentfulStandardPage(slug: {eq: "about-us"}) { title blocks { ... on ContentfulHeroBlock { id name title image { image { fluid { src } } } } ... on ContentfulTextBlock { id bodyContent { bodyContent } } } } }

This unfortunately will not work for us. The way Gatsby/GraphQL work together is that a schema is built during the build, which tells us what content will be available to query for the Standard Page. If our Standard Page will always reference the HeroBlock content type as well as the TextBlock content type, then we are fine.

However, if we ever remove the reference to the HeroBlock in Contentful, our code will break. This is because Gatsby/GraphQL creates a schema from Contentful based on the content types that are set in your Standard Page at the time the static pages are generated. So if you just add your call for the HeroBlock, and there is no HeroBlock referenced by a Standard Page in Contentful at the time, the build will break.

If we need to update code as we update the content model, that's one thing. But we don't want to be in a situation where we need to update code as we update content. So we need to come up with a better way.

Luckily there is one. The solution we came up with is the following:

query MyQuery { contentfulStandardPage(slug: {eq: "about-us"}) { title blocks { __typename ... on Node { ... on ContentfulHeroBlock { id name title image { image { fluid { src } } } } ... on ContentfulTextBlock { id bodyContent { bodyContent } } } } } }

The line to call out here is the on Node portion. If you want the content in Contentful to be truly dynamic, you need this to your call. Node just indicates you want an object with an Id, regardless of the type. Now your call will pass regardless of what content types are referenced on your Standard Page, because all possible references are considered a Node type.

Rendering with React Components

Assuming you have different React components to represent these different content types, your first thought might be to implement either a switch statement or multiple if/else's to determine which component to use based on the GraphQL query results.

But after you sit with it for a minute, you'll probably come to the same conclusion we did. Which is that the if/switch statement approach doesn't scale. Instead, we built a component to manage a collection of content called a "ContentArea" component.

const ContentArea = ({contentTypes}) => { if (contentTypes !== null) { return contentTypes.map((item, index) => { const DynamicComponent = ComponentList[item.sys.contentType.sys.id] if (DynamicComponent !== undefined) { return (<DynamicComponent key={index} content={item} />) } else { return (<></>) } }) } else { return (<></>) } } export default ContentArea

This is how we reference the ContentArea component from our standard-page.js template:

<ContentArea contentTypes={page.blocks} />

We pass in all our page content, the ContentArea component loops through each item, and checks it against a dictionary we call ComponentList to determine which component to use. Going forward, every time you add a new content type that can be referenced on the Standard Page, you can add it to your ComponentList dictionary.

const ComponentList = { 'heroBlock' : Hero, 'textBlock': TextBlock, 'fiftyFifty': FiftyFifty, 'image': ContentfulImage }

And there you have it! The dynamic power of Contentful in Gatsby.

Next Steps

If you haven't already, next I would recommend looking into creating Fragments. Because you might be making similar GraphQL calls from other content types, its good to separate out that logic to avoid repeated code. This will ensure your code continues to be dynamic and scalable. You can check out a few examples of Fragments I made as gists in Github.

As always, feel free to reach out to me or our team if you have any questions.

Black and white profile photo of David Boland

David Boland

Blogging about web, mobile, and content management systems. I am an Optimizely MVP, and a Contentful certified developer.