Scalable content modeling for Contentful and Gatsby

Updated February 23rd 2021

This guide will tell you how to create a scalable and flexible content model with Contentful and Gatsby.

Contentful provides a flexible and loved headless content backend for many Gatsby applications. Getting started with Gatsby and Contentful is easy with the official gatsby-source-contentful plugin, and editing content types and adding new content is a breeze using Contentful's web app.

However, it's also easy for teams to paint themselves into a corner when they introduce new content types without thinking about the overall architecture of their content model.

The purpose of this guide is to describe a technique to model your content in a way that's scalable and allows content editors to not only add new blog posts but also build new landing pages without the help of developers. The architectural pattern that is presented here could be described as a block-based design or block-based architecture.

Sidenote: I originally learned about blocks from a presentation given by one of Contentful's own developers. Back then I had finished my first Contentful project where we had applied a somewhat block-based architecture but lacked a proper term to describe what we were trying to achieve.

Table of contents

The five categories of content types

Your content types are all going to fall into the following five categories:

Applications
Instead of multiple application content types, you are likely to have just one application content type and one entry where you hold all the general editable config of your site. Multi-site setups have multiple entries of the application content type but not multiple application content types (unless needed).

Layouts
The layout content type allows content editors to define the repeating elements of different pages (such as the navbar and footer). This is an optional layer of design since while many sites have multiple page types, they often have just one layout which means the repeating elements can be "hard coded" inside Gatsby.

Pages
A page content type is almost like a page template. Each page type gets its own content type.

Blocks
Blocks are the different headers, sections and other UI elements that content editors can add on layouts and pages.

Values
Value content types take a group of fields and extract them into new concepts that can be reused across the site. A typical value content type is an author with name and image that blog post entries can link to.

In the next chapters I'll show you what these different content types look like in practice. Instead of using Contentful's web UI to create the content types, I will use Contentful's migration scripts that can be run using the Contentful CLI.

You can apply the same changes using Contentful's web app but the migration files make it easier to describe the different types and fields in written form. In addition, if you aren't using migrations yet, I would really recommend you to look into them; once you learn how to write Contenful migration scripts, you will keep using them especially when you need to refactor something in your current content model.

You can find all the migration scripts presented in this guide in their full form here. To read more about how to do migration scripting, see this tutorial.

Application content types

There's usually some general site config that you want content editors to be able to change by themselves. For example, we might want content editors to be able to change the base meta title of our meta title template or the site logo. In order to achieve this, we need to have some place where we can store this information that doesn't belong to any specific site.

To add a new content type to our content model, we create a new migration file. Contentful's migration files all export a function that takes a migration object as its parameter:

module.exports = function (migration) {};

We create a content type for the application with id set to application, name set to Application, and display field (or entry title field) set to name field.

module.exports = function (migration) {
  const application = migration
    .createContentType("application")
    .name("Application")
    .displayField("name");
};

Let's add the name field that we are using as the display field:

module.exports = function (migration) {
  // ...

  const name = application.createField("name");
  name.name("Name").type("Symbol").required(true);
};

The other fields in your application content type will be very specific to the needs of your site's setup. But let's add that base meta title as an example to our application content type:

module.exports = function (migration) {
  // ...

  const baseMetaTitle = application.createField("baseMetaTitle");
  baseMetaTitle.name("Base Meta Title").type("Symbol").required(true);
};

After creating the application content type, create a new entry for it and add the initial config by filling out the content fields.

Block content types

Our layout and page content types require us to have some block content types ready. Because of this, I'm showing you how to create the block content types at this point—before creating any layout or page content types.

When building a site, you are probably not able to add all the block content types needed prior to adding any layout and page content types. This is okay; you can keep adding new blocks and updating block fields in your layout and page content types accordingly.

Generic and specific blocks

If you create a separate block content type for each one of your page elements, you might run into your tier's content type limits. In addition to this, having tons of different block content types can make it difficult for content editors to remember what each content type does and teach others in their team how to manage the content.

One of the most effective ways of keeping the number of block content types under control is to identify which UI elements get constantly reused and refurbished with new content and which elements might get reused but don't necessarily see any changes in their content or other config.

An example of a UI element that get reused and refurbished often is a hero block that contains a heading and a background image. Each landing page usually has one but the text and image inside the heroes changes based on the purpose of the landing page.

An example of a UI element that might get added on every page, but doesn't change much in terms of its content, is a footer block. Footers aren't usually modified to fit specific pages but instead contain the same privacy policy links, copyright notices, and other info from one page to another.

What we want is to have a specific content type for hero blocks but a generic content type for footer blocks. Let's start with the migration script for our example hero block.

Adding specific block content types

As an example of the specific, non-generic block content type, let's add a hero block to our content model.

We start by creating a new content type heroBlock with a name field:

module.exports = function (migration) {
  const heroBlock = migration
    .createContentType("heroBlock")
    .name("Hero Block")
    .displayField("name");

  const name = heroBlock.createField("name");
  name.name("Name").type("Symbol").required(true);
};

Our application content type had also a name field that was used as the content's entry title. I have found it to be a good practice to give every content type a name field that allows content editors to name different blocks or pages in a way that makes it easy for them to find specific entries from a long list of content.

To complete our hero block, let's add fields for the hero heading and image:

module.exports = function (migration) {
  // ...

  const heading = heroBlock.createField("heading");
  heading.name("Heading").type("Symbol").required(true);

  const image = heroBlock.createField("image");
  image
    .name("Image")
    .type("Link")
    .linkType("Asset")
    .validations([{ linkMimetypeGroup: ["image"] }]);
};

Adding the generic block content type

As mentioned earlier, in order to keep our content model more manageable for content editors, we don't necessarily want to create a new block content type for each UI element.

The generic block content type is a content type that can be used to add individual entries for different types of UI elements. Each entry of the generic content type is mapped to a specific React component.

Because generic block entries are mapped to React components, these block entries are added by developers and not content editors. In other words, a content editor can't add a new block entry for a generic block content type without the help of a developer.

However, as mentioned before, it's not likely that content editors need to add for example a second footer or a second navbar to the site. It's often enough that the blocks for the footer and navbar exist as entries and that the content editors can link to them inside different layouts and pages.

It's still possible for content editors to add a second footer using the generic block content type without developers if they really need to. It's just not going to be as user-friendly as it is to add a second hero block.

Let's start building a new migration script for the generic block content type by creating the content type and adding a name field to it:

module.exports = function (migration) {
  const block = migration
    .createContentType("block")
    .name("Block")
    .displayField("name");

  const name = block.createField("name");
  name.name("Name").type("Symbol").required(true);
};

We add a field for the React component and include some validation logic to check that the field value is a valid React component name:

module.exports = function (migration) {
  // ...

  const component = block.createField("component");
  component
    .name("Component")
    .type("Symbol")
    .required(true)
    .validations([{ regexp: { pattern: "^[A-Z][a-zA-Z0-9]*$" } }]);
};

Finally, we add a field for the component properties. These properties are saved as a JSON object.

module.exports = function (migration) {
  // ...

  const properties = block.createField("properties");
  properties.name("Properties").type("Object");
};

Component properties allow content editors to change things like marketing copy or color theme. Editing JSON is not going to be as easy as editing content fields but the assumption is that editors rarely need to do this type of editing.

Deciding which blocks get their own content type and which don't seems to be a balancing act between different concerns. Remember that having a content type for each one of your UI elements can make your content model more confusing to use or cause issues with the content type limits of your current Contenful plan.

Layout content types

The layout content type is going to enable you to extract some of the repeating page elements into their own entries. This is an optional layer of the content model design and it might be enough for you to simply hardcode navbars and footers to your pages inside Gatsby.

If you do want to add layouts to your content model, you need only one content type. Let's create this content type by creating a layout content type with a name field:

module.exports = function (migration) {
  const layout = migration
    .createContentType("layout")
    .name("Layout")
    .displayField("name");

  const name = layout.createField("name");
  name.name("Name").type("Symbol").required(true);
};

Layouts contain the blocks that are repeated from page to page. These blocks are placed before or after the actual content of a given page. We add separate fields for these before and after areas:

module.exports = function (migration) {
  // ...

  const before = layout.createField("before");
  before
    .name("Blocks added before page content")
    .type("Array")
    .items({
      type: "Link",
      linkType: "Entry",
      validations: [{ linkContentType: ["heroBlock", "block"] }],
    });

  const after = layout.createField("after");
  after
    .name("Blocks added after page content")
    .type("Array")
    .items({
      type: "Link",
      linkType: "Entry",
      validations: [{ linkContentType: ["heroBlock", "block"] }],
    });
};

Notice that we have to list the IDs of our block content types when defining the validation logic for both the before and after fields. By validating the content type of each linked item we can make sure that content editors are not able to add any other content besides blocks to these fields.

These lists of valid block IDs can be updated every time you add a new block content type.

Page content types

Each page content type could be thought of as a separate page template. In this guide, we will create one page content type for articles and one default page content type that can be used for things like the homepage and the about page.

Adding a default page content type

Let's start with the default page content type. We create a migration that creates the page content type and adds the name field to it:

module.exports = function (migration) {
  const page = migration
    .createContentType("page")
    .name("Page")
    .displayField("name");

  const name = page.createField("name");
  name.name("Name").type("Symbol").required(true);
};

Next, we create a field for the page slug (slug is a part of the URL path that is used to identify different pages from each other). We also change the appearance of this field to a slug field control:

module.exports = function (migration) {
  // ...

  const slug = page.createField("slug");
  slug
    .name("Slug")
    .type("Symbol")
    .required(true)
    .validations([{ unique: true }]);
  page.changeFieldControl("slug", "builtin", "slugEditor");
};

We add a layout field to link each page to a specific layout:

module.exports = function (migration) {
  // ...

  const layout = page.createField("layout");
  layout
    .name("Layout")
    .type("Link")
    .linkType("Entry")
    .required(true)
    .validations([{ linkContentType: ["layout"] }]);
};

Finally, we add a field for the actual content of the page which in our case consists of different blocks. As is the case with the layout content type, we have to make sure that content editors can only add blocks (and not other content) to the field by listing all the IDs of the available block content types inside our item validations:

module.exports = function (migration) {
  // ...

  const blocks = page.createField("blocks");
  blocks
    .name("Blocks")
    .type("Array")
    .items({
      type: "Link",
      linkType: "Entry",
      validations: [{ linkContentType: ["heroBlock", "block"] }],
    });
};

Adding an article page content type

We can add another page content type for article pages to demonstrate how we might structure other, more specific page content types.

We first create the content type itself and add the same name, slug, and layout fields that we added to the default page content type:

module.exports = function (migration) {
  const articlePage = migration
    .createContentType("articlePage")
    .name("Article Page")
    .displayField("name");

  const name = articlePage.createField("name");
  name.name("Name").type("Symbol").required(true);

  const slug = articlePage.createField("slug");
  slug
    .name("Slug")
    .type("Symbol")
    .required(true)
    .validations([{ unique: true }]);
  articlePage.changeFieldControl("slug", "builtin", "slugEditor");

  const layout = articlePage.createField("layout");
  layout
    .name("Layout")
    .type("Link")
    .linkType("Entry")
    .required(true)
    .validations([{ linkContentType: ["layout"] }]);
};

Instead of the blocks field, we want to add a rich text field for the article content. This field will make it easier for content editors to create article pages that consist mainly of written text with different heading levels and paragraphs.

We can call this field body:

module.exports = function (migration) {
  // ...

  const body = articlePage.createField("body");
  body.name("Body").type("RichText");
};

We still want to give editors the option to add blocks to specific article pages if they want to. To achieve this, we add before and after fields to the article page content type. Both of these fields are used to link specific blocks to the page. The before field is reserved for blocks that should be displayed before the body text and the after field for blocks that should be displayed after the body text:

module.exports = function (migration) {
  // ...

  const before = articlePage.createField("before");
  before
    .name("Blocks added before body text")
    .type("Array")
    .items({
      type: "Link",
      linkType: "Entry",
      validations: [{ linkContentType: ["heroBlock", "block"] }],
    });

  const after = articlePage.createField("after");
  after
    .name("Blocks added after body text")
    .type("Array")
    .items({
      type: "Link",
      linkType: "Entry",
      validations: [{ linkContentType: ["heroBlock", "block"] }],
    });
};

Value content types

You may want to abstract some of the content into their own content types so that you can reuse them inside other content entries. For example, if we want to add authors to our article pages, we should create a new author content type so that author content entries can be linked to multiple different articles.

These value content types are not blocks because they don't really correspond to an UI element that content editors can add on their pages between other blocks. Value content types simply take some set of fields and combine them to form a new concept to our content model.

As an example, let's add authors to our article pages.

First, we create the author content type with author name and avatar:

module.exports = function (migration) {
  const author = migration
    .createContentType("author")
    .name("Author")
    .displayField("name");

  const name = author.createField("name");
  name.name("Name").type("Symbol").required(true);

  const avatar = author.createField("avatar");
  avatar
    .name("Avatar")
    .type("Link")
    .linkType("Asset")
    .validations([{ linkMimetypeGroup: ["image"] }]);
};

Next, we need to add a field to our article page that links to author entries. Let's edit the existing article page content type inside the current migration script:

module.exports = function (migration) {
  // ...

  const articlePage = migration.editContentType("articlePage");
  const authorField = articlePage.createField("author");
  authorField
    .name("Author")
    .type("Link")
    .linkType("Entry")
    .validations([{ linkContentType: ["author"] }]);
};

Conclusion

This guide laid out a way for you to build a scalable and flexible content model for a site built with Gatsby and Contentful. I hope you found it helpful. If so, you can also help others by sharing this guide on Twitter or wherever else.

You can find all the migration files presented in this guide here.

Subscribe by RSS
© 2021