Adding Netlify CMS to Gatsby (Part 1)

As I've been building sites with Gatsby for a little while, and deploying on Netlify, it makes sense to try out Netlify's own CMS and take it out for a spin. I've had a poke around Gatsby's Netlify CMS starter template, but so much has already been done that I can hardly work out where Gatsby ends and the CMS begins. I want to understand how to add Netlify CMS to an existing project.

What do they each do?

It can be tough to work out when you look at the Gatsby Netlify CMS Starter exactly what is being done by each part, and how it ends up producing a finished site. screen shot 2019 08 05 at 20 42 29

The important place to look on this diagram is where the two cross over. In the right-hand bubble, Gatsby builds a static site from a data source, in this case from markdown files and images. As a developer, markdown is easy to work with and edit - the CMS isn't entirely needed. In fact you could remove Netlify CMS from this starter and you'd still have a fully functioning Gatsby site based on the collection of markdown + frontmatter and images. (In case you're new to the term, frontmatter refers to key/value formatted information placed before the markdown content.) This eliminates the need for a database and keeps all site data visible and committed within the repository. What Gatsby (and its plugins) does with these files is transform them to queryable GraphQL data, render markdown to HTML and optimise and transform images. This can then be used to programatically render pages (like blog posts) and fill static pages with content via GraphQL queries.

On the left-hand side, what Netlify CMS adds is a quick to configure setup to manage this content. This isn't so necessary for a developer while you're buliding the site - you can pop an image file directly into your repo, and write content into markdown files in a flash - but when you want to hand over the site to your client and allow them to update and add content, it provides a nice clean interface, you can add users via Netlify and it takes care of authenticating them securely without any need to code a backend server. You can use validation and required fields to make the process relatively foolproof. When content is added via the CMS, it is committed directly to the Git repo, keeping the repo updated as a single source of truth for the site and all content and triggering a new build on Netlify with the up-to-date content.

  • It's serverless, you don't need to run a backend CMS server anywhere
  • It's quick to configure
  • Github is a single source of truth, all content is visible in the repo
  • It is designed to integrate well with modern static site generators
  • It already has widgets to handle things like file and image uploads, so you don't have to worry about how to do these things
  • It can be customised to add whatever content you like
  • It can generate markdown, which Gatsby digests very well
  • Netlify Identity gives you a quick and secure method of authenticating users for the CMS, and these users cannot alter the Git repo except to change the CMS
  • Everything you upload to the CMS is committed to the Git repo, so it can get very bloated (unless you use cloud storage or Git LFS)
  • This could lead to long build times while Netlify downloads the repo
  • If you have a public repo, all your assets are visible

Adding Netlify CMS to my existing Gatsby project

I've made a personal website for myself using Gatsby's default starter, and I'd like to add a CMS to manage two parts of the site. Firstly for the "Recent Work" section on the homepage, and then later I'd like to add a blog that I can update via the CMS (see part 2).

I've added support for SASS to gatsby-config, imported rules from Tachyons using tachyons-sass for styling and added gatsby-plugin-purgecss to remove any CSS rules from Tachyons that I don't use. I love how quick and easy it is to get all this set up - the more I work with Gatsby, the more I come to appreciate how much heavy lifting it can do for you.

In the Work component above, each item of recent work is rendered like so... (Full Bleed is a component I built using a layout style from Tachyons)

const Work = ({ work, ...props }) => (
  <SectionContainer {...props}>
    <SectionHeading>Recent Work</SectionHeading>
    <FullBleed>
      {work.map(({ imageURL, client, project, colors, about }) => (
        <FullBleed.Tile backgroundURL={imageURL}>
          <FullBleed.HiddenOverlay className={colors}>
            <Client>{client}</Client>
            <Subtitle className="mb4">{project}</Subtitle>
            {about.map(paragraph => (
              <P>{paragraph}</P>
            ))}
          </FullBleed.HiddenOverlay>
        </FullBleed.Tile>
      ))}
    </FullBleed>
  </SectionContainer>
);

And in index.js I have hardcoded the recent work as an array of objects with the relevant keys:

const work = [
  {
    client: "XL Recordings",
    project: "Anima Technologies",
    imageURL: animaTechnologies,
    colors: "bg-orange dark-gray",
    about: [
      "A series of interactive voice and SMS flows, part of the release campaign for Thom Yorke's album Anima.",
      "Twilio was used to create an SMS chatbot and telephone answering service which engaged tens of thousands of users in the space of a few days.",
    ],
  },
  {
    client: "Supply Change CIC",
    project: "supplychange.co.uk",
    imageURL: supplyChange,
    colors: "bg-dark-blue light-pink",
    about: [
      "Working within an existing React and Express codebase, I implemented a form component which allows users to search for and add their company details using the Companies House API.",
    ],
  },
  {
    client: "Verdigris Management",
    project: "Hot Chip Megamix",
    imageURL: hotChip,
    colors: "bg-light-red dark-blue",
    about: [
      "A React application with a customised Soundcloud player to accompany a mix with animated visuals and a provide users with a listing of tour dates via the Songkick API when available.",
    ],
  },
  {
    client: "Lacuna Frames",
    project: "lacunaframes.co.uk",
    imageURL: lacunaFrames,
    colors: "bg-dark-green light-pink",
    about: [
      "A simple site layout that retains semantic HTML layout, but is presented as a rotating frame with text that grows and shrinks into view with the angle of rotation.",
    ],
  },
  {
    client: "East End Trades Guild",
    project: "RentCheck App",
    imageURL: eastEndTrades,
    colors: "bg-orange near-white",
    about: [
      "A rapid MVP development which supported a successful bid for further funding from Hackney and Tower Hamlets councils. Built using React, Express, OpenStreetMap and Airtable.",
    ],
  },
];

Initial setup of Netlify CMS

Now to get Netlify CMS into my project. I follow add to your site in the official docs which tells me the CMS requires two files to be served up from the /admin folder, so you could add them to public/static/admin

  • index.html will serve up the necessary scripts to run the CMS
  • config.yml will hold the schema for the CMS

But it's a little confusing, as these instructions are written without a particular framework in mind. Of course, Gatsby has its own plugin to support the generation of a CMS page so you can npm install gatsby-plugin-netlify-cms and add it to gatsby-config (read instructions on npm)

You can ignore the official Netlify docs for a moment and follow the setup using the above link for gatsby-plugin-netlify-cms.

  • create static/admin folder in your project root
  • create static/img to store image uploads (make sure you add a .gitkeep file to commit this folder or your build will fail on Netlify like mine did at first)
  • create config.yml in static/admin

Following the docs for the plugin linked above, and looking at gatsby-starter-netlify-cms to see how they're doing it, my config.yml looks like this:

backend:
  name: git-gateway # Link the CMS with a Git repo via Netlify
  branch: master # Branch to update (optional; defaults to master)
media_folder: static/img # Store uploaded images in this folder
public_folder: /img # Where to find the images in the live site

Now I need to define a "collection", which seems to be what Netlify calls a schema to define a type of content. Based on the fields I want for my "Recent Work" component, my collection is defined like this:

collections:
  - name: "work" # Used in routes, e.g. admin/collections/work
    label: "Work" # Used in the CMS UI
    folder: "src/pages/work" # Where the files will be stored
    create: true # Users can create new posts
    slug: "{{slug}}" # Creates a safe slug from the post's title, which will be used for the post's filename
    fields: # The fields for each document
      - {label: "Title", name: "title", widget: "string"}
      - {label: "Client", name: "client", widget: "string"}
      - {label: "Image", name: "image", widget: "image"}
      - {label: "Colours", name: "colours", widget: "string"}
      - {label: "About", name: "about", widget: "text"}

I think I need the Title attribute, so I'm going to use that instead of project and refactor my component accordingly. Another look at the docs tells me I was right ðŸĪ“, because by default title is the field Netlify CMS uses to identify each post, however you can change it to another field if you want to using identifier_field. The widget field chooses what kind of content each field can use and renders a field for it in the editor. I was saving my description as an array of strings to be rendered as paragraphs, but I'll see if the CMS's text widget can achieve something similar (it claims to support multiline text).

It's time to run npm start and see what happens! I'm not all that surprised to get an error ðŸĪŠ. I've got some problems with my yaml file and I have to remove some commas (I've already removed them from the example above) which are causing the CMS to fail. Now when I run npm start I get a link telling me that Netlify CMS is running at http://localhost:8000/admin/. Excellent!

But when I follow the link I've got another error - 'collections[0].slug' should be string. This took a while to figure out (a quick Google had me thinking I might need to create a file structure for the CMS to write into, but thankfully not). My mistake was not to put quotes around {{slug}}. Yaml isn't very forgiving, but I think I've got it configured now.

Hooray! The CMS seems to be working

But... it wants to know where on Netlify my site is hosted... I didn't deploy to Netlify yet so I guess I'd better do that!

I've logged into Netlify and deployed the site from my Github repository. A couple of gotchas cropped up:

  • The suggested build script gatsby build doesn't work. It would work on my local machine because I have gatsby-cliinstalled globally, but crashes the build on Netlify. I used npm run build instead, which invokes gatsby-build from npm, and this works fine.
  • Once the build was running, I got a failed deploy because the /static/img folder referenced in config.yml didn't exist yet. I put a .gitkeep file in the folder so it gets committed to Github.
  • I had made a couple of mistakes in imports where my capitalisation didn't match the filename perfectly. This went unnoticed on my local machine as Mac OS is case-insensitive but did need fixing for Netlify.

After fixing these errors, the site is deployed and live 🎉 - now to try and access the CMS...

I'm waiting...

And... another error ðŸĪĶðŸŧ‍♂ïļ. This time it's telling me that it Failed to load settings from /.netlify/identity

Of course, I need to set up Netlify Identity from the Netlify control panel for my site.

It's pretty straightforward - I enable Identity and then I can invite myself as the first user. I receive an email straight away and follow the link to choose my password.

And... that's still not working. I also need to enable Git Gateway in the settings for Identity before the CMS can run.

YAY! I can see an editor panel with my "Work" collection set up. An API error flashes up... I hope this is because no posts exist yet... I don't see this again so I'm pretty sure we're ok.

I am able to add some items to the "work" collection, and when I save them I can see the files that are generated in the repo on Github! Awesome!

Using the CMS data in my Gatsby site

So now the posts have been saved as markdown files in src/pages/work. How can I access these and display them in my site? I love that Gatsby uses GraphQL as standard, and I want to be able to access the CMS data via GraphQL queries in my pages.

Gatsby has some info on Adding Markdown Pages which might help...

Looks like I need to use gatsby-source-filesystem to read the markdown files that are committed by the CMS. I'm going to add the following to my gatsby-config plugins:

  {
    resolve: `gatsby-source-filesystem`,
    options: {
      name: `work-posts`,
      path: `${__dirname}/src/pages/work`,
    },
  },

And, from reading the docs linked above, I see that I need to add gatsby-transformer-remark after the source plugins to import the data from my markdown files into GraphQL. Once I've done this, I can find the posts I added in the GraphiQL IDE

This will work for me! I'm pleased to see that the text widget has preserved the newline characters, which I can use to split the text into an array of paragraphs as it was in my hardcoded object.

I'm going back to my index.js page to add a page query (see Gatsby's guidance on adding a GraphQL query to a specific page)

export const query = graphql`
  query homePageQuery {
    allMarkdownRemark {
      edges {
        node {
          id
          frontmatter {
            project: title
            image
            colours
            client
            about
          }
        }
      }
    }
  }
`;

Now I can pass this data through to my Work component instead of the object I had before. I've aliased title as project in the query so that the data object returned will have a project field instead of title. With a little bit of tweaking, my component now displays the items from the CMS. And I've got a post ID to use as a key for my map. Yay!

const Work = ({ work, ...props }) => {
  return (
    <SectionContainer>
      <SectionHeading>Recent Work</SectionHeading>
      <FullBleed>
        {work.map(
          ({
            node: {
              id, frontmatter: { image, client, project, colours, about },
            },
          }) => (
            <FullBleed.Tile backgroundURL={image} key={id}>
              <FullBleed.HiddenOverlay className={colours}>
                <Client>{client}</Client>
                <Subtitle className="mb4">{project}</Subtitle>
                {about.split("\n\n").map(paragraph => (
                  <P>{paragraph}</P>
                ))}
              </FullBleed.HiddenOverlay>
            </FullBleed.Tile>
          )
        )}
      </FullBleed>
    </SectionContainer>
  );
};

Styling the post preview in the CMS

At the moment, my post preview displays a bland list of fields. Wouldn't it be nice if I could see what my post would look like on the site?

Netlify CMS renders using Javascript, so I can use React to create a template to render a post dynamically while it's being edited.

I need to tell the plugin that I've got a custom CMS module by editing gatsby-config and adding a modulePath

    {
      resolve: "gatsby-plugin-netlify-cms",
      options: {
        modulePath: `${__dirname}/src/cms/cms.js`,
      },
    },

Next, I need to install netlify-cms-app (not netlify-cms as one of the docs I read told me, as that has been deprecated) and import it into a new file called cms.js at the path above.

Then I can create and register preview templates for the CMS to use.

import CMS from "netlify-cms-app";

import "../components/styles.scss";

import workPreview from "./preview-templates/workPreview";

CMS.registerPreviewTemplate("work", workPreview);

I take a peek at gatsby-starter-netlify-cms to see how they are making preview components, and follow their lead. I need to create /preview-templates/workPreview.js in my cms folder and tell it how to preview the post.

First, I'm going to split up my Work component into a container and a WorkItem component, so I can export this component that renders one individual item of work, and use this to render my preview. I like that this method uses the actual component from my site, because any changes I make to this component in the site will be directly reflected in the CMS preview. This refactoring has the satisfying effect of allowing me to spread {...frontmatter} when passing it into the WorkItem component rather than destructuring the props, and gives me the post id which I can use as a key.

I've also added props for the width and aspect ratio styles to WorkItem and to my FullBleed.Tile component, which makes them more flexible and reusable, and also means I can pass through slightly different styles in the preview if I want to.

I also discovered by trial and error that the component would crash in the preview without adding a check to see if about has a value before trying to split it (line 30 below). This didn't matter on my site as I know there will be a value, but in the preview it can start with a blank input.

export const WorkItem = ({
  image,
  client,
  project,
  colours,
  about,
  slug,
  width,
  aspectRatio,
}) => (
  <FullBleed.Tile backgroundURL={image} width={width} aspectRatio={aspectRatio}>
    <FullBleed.HiddenOverlay className={colours}>
      <Client>{client}</Client>
      <Subtitle className="mb4">{project}</Subtitle>
      {about && about.split("\n\n").map((paragraph, i) => (
        <P key={i}>{paragraph}</P>
      ))}
    </FullBleed.HiddenOverlay>
  </FullBleed.Tile>
);

export default ({ work, ...props }) => {
  return (
    <SectionContainer>
      <SectionHeading>Recent Work</SectionHeading>
      <FullBleed>
        {work.map(({ node: { id, frontmatter } }) => (
          <WorkItem
            key={id}
            width="w-100 w-50-ns w-third-l"
            aspectRatio="aspect-ratio--1x1 aspect-ratio--3x4-m"
            {...frontmatter}
          />
        ))}
      </FullBleed>
    </SectionContainer>
  );
};

To use the WorkItem component in my preview template, I need to import it, and then use the entry prop to look up the relevant fields. I'm going to preview with a width of 100% of the preview pane, but also set a max width. I've decided I want my preview to display only as square, although it will resize responsively on the site.

import * as React from "react";

import { WorkItem } from "../../components/work";

export default ({ entry, widgetFor }) => (
  <div className="avenir">
    <WorkItem
      width="w-100 mw6"
      aspectRatio="aspect-ratio--1x1"
      image={entry.getIn(["data", "image"])}
      project={entry.getIn(["data", "title"])}
      client={entry.getIn(["data", "client"])}
      about={entry.getIn(["data", "about"])}
      colours={entry.getIn(["data", "colours"])}
    />
  </div>
);

Now to load the necessary styles into the CMS editor to style my components. gatsby-plugin-netlify-cms's docs on npm tell me that I can import styles directly into cms.js and they will load into the CMS. I tried this and it took a while to get the styles to actually show up in the preview pane - I had to run npm run build before the preview styles had any effect at all, although now I've done this I can see that they also have the undesired effect of normalising the editor's default styles (and so stripping away the existing font - Tachyons uses normalize.css to remove styles before adding its own which I think is conflicting with the existing styles in the editor).

Unfortunately, this means I can't load my whole site's styles into the CMS via an import statement without mucking up the editor...

I can see 3 options to fix this:

  • add a script to compile the site's styles at build time so they are available from admin/preview.css and load this with CMS.registerPreviewStyle("admin/preview.css")
  • add additional CSS rules on top of my site's styles to nicely style the CMS editor's fonts and then add the styles into cms.js with import statements.
  • because my site's styles are based on (mostly) out-of-the-box Tachyons, I can load tachyons.css from a CDN with CMS.registerPreviewStyle

I go with loading Tachyons from a CDN as the quickest fix for now, although I'll need to choose one of the other options if my site's styles diverge from the off the shelf Tachyons styles.

And now my post is nicely styled in the preview pane! I like this a lot, because I can play with the colour settings in the editor before I commit.

Adding validation

I've built this CMS to use myself, and since I know the limitations of my site I know that, for example, not uploading an image for a piece of work will make my site look totally crap. But I build sites for clients, and they need to be pretty foolproof so I want to know how to validate input and enforce required fields.

Going back to my config.yml, I can add required: true to any fields I want to make required. That seems easy enough. I'm setting all the fields to required, except for colours, which I will also give a default value in case nothing is entered.

Now my collections config looks like this:

collections:
  - name: "work" # Used in routes, e.g. admin/collections/work
    label: "Work" # Used in the UI
    folder: "src/pages/work" # Where the files will be stored
    create: true # Users can create new posts
    slug: "{{slug}}" # Creates a safe slug from the post's title
    fields: # The fields for each document
      - { label: "Title", name: "title", widget: "string", required: true }
      - { label: "Client", name: "client", widget: "string", required: true }
      - { label: "Image", name: "image", widget: "image", required: true }
      - {
          label: "Colours",
          name: "colours",
          widget: "string",
          default: "bg-near-black near-white",
          required: true
        }
      - { label: "About", name: "about", widget: "text", required: true }

And when I try to create and publish a new post, I am pleased to see that I get an error message for a missing field, and my default colour classes are already filled in.

Taking a look at the widgets docs it looks like there is support for validation, but only by regex. Anything else requires you to build a custom widget. On the positive side, you do get to write a custom error message.

I'm going to add validation to the colours field, which should only accept two Tachyons classes in the format of bg-mybgcolour mytextcolour. Using RegExr I've played around to find a pattern that seems to work.

/^(bg-[a-z]+ [a-z]+||[a-z]+ bg-[a-z]+)$

In my config.yml I add it like this:

      - {
          label: "Colours",
          name: "colours",
          widget: "string",
          default: "bg-near-black near-white",
          required: true,
          pattern:
            [
              "^(bg-[a-z]+ [a-z]+||[a-z]+ bg-[a-z]+)$",
              "Please provide two Tachyons classes for background and foreground colour",
            ],
        }

Right away I can run npm start and test it out on my local machine. And it seems to work nicely! As you can see below, I can't publish this post with a missing image field, or without both colours classes.

I could make my regex more specific, perhaps by including all possible options. Right now it would also allow other classes, but it gives a good reminder to add the necessary info. I'm not exactly a regex expert (regexpert?) but it does seem like you could perform most validation you need to this way (e.g. string length, permitted or forbidden characters)

Making changes to a collection

Now I've got my collection set up and working as intended, how easy is it to make changes to it?

While I had control over the order of items in my recent work when I hardcoded them as one object, I've noticed that on every build my site now displays work in a completely different order. I'd much rather choose how these are displayed, so I'm going to add a field to take a numerical value so I can sort them.

collections:
  - name: "work" # Used in routes, e.g. admin/collections/work
    label: "Work" # Used in the UI
    folder: "src/pages/work" # Where the files will be stored
    create: true # Users can create new posts
    slug: "{{slug}}" # Creates a safe slug from the post's title
    fields: # The fields for each document
      - { label: "Title", name: "title", widget: "string", required: true }
      - { label: "Client", name: "client", widget: "string", required: true }
      - { label: "Image", name: "image", widget: "image", required: true }
      - {
          label: "Colours",
          name: "colours",
          widget: "string",
          default: "bg-near-black near-white",
          required: true,
          pattern:
            [
              "^(bg-[a-z]+ [a-z]+||[a-z]+ bg-[a-z]+)$",
              "Please provide two Tachyons classes for background and foreground colour",
            ],
        }
      - { label: "About", name: "about", widget: "text", required: true }
      - { label: "Order", name: "order", widget: "number", required: true }

I've added my new field to the end of the collection, now to run it locally and see what happens. Nice! Without having to run npm start again, the dev server has updated the CMS - as soon as I refresh the CMS page I can see the order field and I can add the value to each of my records.

I add a sort method to my work component and restart the dev server to see what happens. As always, I'm getting ahead of myself - I realise that I haven't updated the graphQL query in index.js, so the order field won't be visible. I make the necessary changes and get error messages!

error  Cannot query field "order" on type "MarkdownRemarkFrontmatter"

Oh... I made changes to items in my CMS, but that will have been committed to the remote repo. I need to git pull to see the changes locally. Once that's done my work page is displaying in order.

But I'm not 100% happy with the order and I want to change it. I can make the changes locally by editing the markdown files themselves - if I push the changes up to Github this should also update the CMS. Let's see! Once I've made the changes locally, I can see them reflected on localhost:8000 immediately - I didn't even need to restart the dev server. I push up my changes and (once build has finished) the changes are visible on the live site. I log into the remote CMS and my changes are visible there too. Excellent!

Takeaways

Overall I'm impressed by how easy this was to set up. It's easy to configure, easy to change and easy to use. I like that the content is all visible in the repo, not obscured away in a database.

Key points:

  • YAML is finicky, be precise
  • Use a .gitkeep to commit folder structure for new content types
  • It would be well worth figuring out how to use Cloudinary or Git LFS to avoid enormous bloated repos
  • Don't forget to set up Netlify Identity and Git Gateway in order to access your CMS
  • Validation is easy to set up, but only regex-based validation is available without building a custom widget
  • Importing CSS to the editor with reset / normalize rules can overwrite existing editor styles