Adding Netlify CMS to Gatsby (Part 2)

In my previous post, I covered adding Netlify CMS to a Gatsby project from scratch, creating a collection of content, validating input and accessing the CMS content from GraphQL in Gatsby.

Now I'd like to add a blog to my site - for this I want not only to access the content via graphQL queries in Gatsby, I also want to programmatically build a page for every blog post, and a blog page to list them all. It might be cool to add pagination to the blog too for when my blog post collection grows.

I've also noticed that the images I've uploaded to the CMS are taking a long time to load in my site - I havent taken advantage of Gatsby's image optimisation. I want to work out how to import the images into Gatsby so I can use them with gatsby-image. This will also be important for my blog posts, as I would prefer for the images in my posts to be imported at build and available locally. Even better if they can be optimised with gatsby-image too.

Adding a blog collection to the CMS

Looking at the config.yml on gatsby-starter-netlify-cms I can see that they've added a pretty sensible set of fields, and that the blog content will be written in markdown. That suits me nicely, so I'm going to more or less copy the blog setup they're using.

One thing I notice is the hidden field for templateKey, which identifies this as a blog post. I think it might be important to add this field to my existing collection of recent work as well. I also notice that the starter repo's example hasn't made any fields required, so I'm going to add that where I think it's needed.

Here is my updated config.yml

# Set up the cms options
backend:
  name: git-gateway # Link the CMS with a Git repo
  branch: master # Branch to update (optional; defaults to master)
media_folder: static/img # Store uploaded images in this folder
public_folder: /img

# Define collections of content
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: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "recent-work",
        }
      - { 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 }

  - name: "blog"
    label: "Blog"
    folder: "src/pages/blog"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {
          label: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "blog-post",
        }
      - { label: "Title", name: "title", widget: "string", required: true }
      - {
          label: "Publish Date",
          name: "date",
          widget: "datetime",
          required: true,
        }
      - {
          label: "Description",
          name: "description",
          widget: "text",
          required: true,
        }
      - { label: "Featured Post", name: "featuredpost", widget: "boolean" }
      - {
          label: "Featured Image",
          name: "featuredimage",
          widget: image,
          required: true,
        }
      - { label: "Body", name: "body", widget: "markdown", required: true }
      - { label: "Tags", name: "tags", widget: "list" }

Once I've saved the changes, my local CMS straight away allows me to write my blog post. It's cool that the markdown editor also allows you to edit in rich text, but markdown is good for me as I'm pasting in from hackmd. It tells me that the Featured Post field is required, which is weird as that's one of the ones I didn't make required, so I have to toggle it on and back off to save the post. I add required: false to that field in the hope that it will resolve the problem.

Getting my blog posts into Gatsby

First I need to pull the blog posts into Gatsby's GraphQL data. I need to add agatsby-source-filesystem plugin to reference src/pages/blog. Next I need to pull down the changes I just made to the CMS, as they have been committed to Github not to my local machine. Great! Now I can see the blog post in the GraphiQL IDE.

Wait a minute... It's been rendered into my recent work section!

I added the templateKey field to my CMS, but I still need to add the key to my work posts, and then I guess I'll need to do some filtering in order to get the right data where I need it. I add a field for templateKey directly to my markdown files for my blog posts. It takes a while to be able to find it in the GraphiQL IDE, and I'm not sure what the delay is, but eventually I can see it under frontmatter: templateKey

I've made a mistake on one file and set the key to brecent-work. Oops! I go into my code editor and change it, and it's super cool to see that the change comes up straight away in the GraphiQL IDE. I've used the gatsby-source-wordpress plugin a bit before, and always found it frustrating that I would have to ctrl+c and restart the dev server to see changes I made to content. This is a much nicer dev experience!

Now I'm going to edit my recent work query to filter out all but recent work items. I just need to add a filter to the query in index.js and my front page is back to normal.

  query homePageQuery {
    allMarkdownRemark(
      filter: { frontmatter: { templateKey: { eq: "recent-work" } } }
    ) {
      edges {
        node {
          id
          frontmatter {
            project: title
            image
            colours
            client
            about
            order
          }
        }
      }
    }
  }

I'm going to need to do two things now - render an individual page for each blog post, and render a blog page which lists all my posts. Let's start by rendering each blog post.

In gatsby-node.js I can write a query to get all my blog posts. I start by writing my query in the GraphiQL IDE, and I'm a bit confused to get a warning telling me I shouldn't filter by templateKey (although the output does show me an appropriately filtered list with my one blog post). I think this is because I didn't ctrl+c and restart the gatsby dev server. I do this and pleasingly I can now see templateKey in the list of filtering options in the left hand panel.

I take a look at gatsby-starter-netlify-cms to see what's going on in gatsby-node.js. Hmm... I've never seen the onCreateNode API being used before. It looks like it's importing images to be used with Gatsby Image. I'll come back to this later. For now I want to build the blog post pages.

I use the example to write my own - I remove the part that generates tag pages. On second glance, the onCreateNode API is being used to create a slug field for each post. I remove the part that does something with images for now.

Now I need a template - because there's nothing yet at src/templates/blog-post.js I'm getting an error when I try to build. I add a page, and use the page query from the example.

import * as React from "react";
import { graphql } from "gatsby";
import Layout from "../components/layout";
import SEO from "../components/seo";

const BlogTemplate = ({ date, title, description, content, tags }) => (
  <>
    <h1>{title}</h1>
    <div dangerouslySetInnerHTML={{ __html: content }} />
  </>
);

const BlogPost = ({ data }) => {
  const { markdownRemark: post } = data;
  return (
    <Layout>
      <BlogTemplate
        title={post.frontmatter.title}
        content={post.html}
        date={post.frontmatter.date}
        description={post.frontmatter.description}
        tags={post.frontmatter.tags}
      />
    </Layout>
  );
};

export default BlogPost;

export const pageQuery = graphql`
  query BlogPostByID($id: String!) {
    markdownRemark(id: { eq: $id }) {
      id
      html
      frontmatter {
        date(formatString: "MMMM DD, YYYY")
        title
        description
        tags
      }
    }
  }
`;

Here's what I end up with after combining my index.js template with the example blog post template. I don't have a component to render HTML so I'm just going to render it directly and see what happens. Maybe it will come out looking how it does on HackMD?

Not quite...

Custom rendering from markdown to React

As I've styled my site with Tachyons, I need to put classes into the HTML elements to style them. I could write some CSS to style the whole post, but that would require considerable work to recreate some of Tachyons' styling, and doesn't take advantage of the modular nature of React components. I could use a markdown processer such as Marked.js but that seems pointless when Gatsby is already doing markdown > HTML processing and will still only produce raw HTML.

I take a look around in the GraphiQL IDE to see if the content is accessible in a different way. Yes! There's something called htmlAst, which appears to be some sort of nested object with a hierarchical tree of HTML elements, much like React's Virtual DOM object. Now we're getting somewhere - I can use this source and parse it into React using my own components. Digging down into Gatsby's docs for using Remark I can see that it's possible to use rehype-react to do this.

I run npm i rehype-react and set up a simple test using the example in the above link.

const PrimaryTitle = tachyons("h1", "f1 fw6 red");
const SecondaryTitle = tachyons("h2", "f2 fw4 gold");
const TertiaryTitle = tachyons("h3", "f3 navy");

const renderAst = new rehypeReact({
  createElement: React.createElement,
  components: {
    h1: PrimaryTitle,
    h2: SecondaryTitle,
    h3: TertiaryTitle,
  },
}).Compiler;

const BlogTemplate = ({ date, title, description, content, tags }) => (
  <>
    <h1>{title}</h1>
    {renderAst(content)}
  </>
);

It works! My h2 headings are now gold. Now to add more components. I add components styled with Tachyons for h1, h2, h3, p, ul, li, img. At first I'm mostly adding margins so the whole block of text isn't so smushed up together. I can use the 'measure' classes of Tachyons to make paragraphs more readable, but the further I go with this, the more I notice how the code blocks have horrible styling (I think Chrome's default) and wonder how I can get nice syntax highlighting for my code blocks. No problem! It's a bit of a shortcut as I have to use useEffect but this article on getting Prism working in React is a nice, quick way to get formatted code blocks up and running.

After playing around with the styling and adding some more components with styling, my template looks like this:

import * as React from "react";
import { graphql } from "gatsby";
import rehypeReact from "rehype-react";
import Layout from "../components/layout";
import SEO from "../components/seo";
import tachyons from "../components/tachyons/tachyonsComposer";
import Prism from "prismjs";
import "./prism-tomorrow.css";

const PostContainer = tachyons("div", "w-100 bg-near-white pv2 avenir");

const PostInnerContainer = tachyons(
  "article",
  "w-90 mw7 center pt2 pt3-ns pt4-l pb5 f6"
);

const Date = tachyons("p", "mt2 f7 tr");

const PrimaryTitle = tachyons("h1", "f1 fw6");

const renderAst = new rehypeReact({
  createElement: React.createElement,
  components: {
    h1: PrimaryTitle,
    h2: tachyons("h2", "f2 fw4 blue mt4 mb4"),
    h3: tachyons("h3", "f3 navy mt3 mb3"),
    p: tachyons("p", "f5 mb3 lh-copy w-100"),
    ul: tachyons("ul", "f5 list pl0 mv3 w-100 pl4 measure-wide", {
      style: { listStyleType: "circle" },
    }),
    li: tachyons("li", "f5 lh-copy w-100 mb3"),
    img: tachyons("img", "mv4 db center"),
    code: tachyons("code", "f7 mv3 br2 navy bg-black-10 pa1 border-box"),
    pre: tachyons("pre", "br3"),
  },
}).Compiler;

const BlogTemplate = ({ date, title, description, content, tags }) => {
  React.useEffect(() => {
    Prism.highlightAll();
  });
  return (
    <>
      <SEO title={title} />
      <PostContainer>
        <PostInnerContainer>
          <Date>{date}</Date>
          <PrimaryTitle>{title}</PrimaryTitle>
          {renderAst(content)}
        </PostInnerContainer>
      </PostContainer>
    </>
  );
};

const BlogPost = ({ data }) => {
  const { markdownRemark: post } = data;
  return (
    <Layout>
      <BlogTemplate
        title={post.frontmatter.title}
        content={post.htmlAst}
        date={post.frontmatter.date}
        description={post.frontmatter.description}
        tags={post.frontmatter.tags}
      />
    </Layout>
  );
};

export default BlogPost;

export const pageQuery = graphql`
  query BlogPostByID($id: String!) {
    markdownRemark(id: { eq: $id }) {
      id
      htmlAst
      frontmatter {
        date(formatString: "MMMM DD, YYYY")
        title
        description
        tags
      }
    }
  }
`;

And my blog post looks lovely! Now to use the BlogTemplate component to provide a preview in my CMS. When I wrote my blog template I made sure to create a BlogTemplate component that I could export to render a blog post (without the rest of the page) elsewhere. I create a new preview template in my CMS folder (Take a look back at Part 1 to see how I started off with CMS preview templates) and import my BlogTemplate component. Simple! Don't be silly, of course it's not going to be that easy. So what do I need to figure out? I have to check with the Gatsby Netlify CMS starter to see how they pass through the post content (hint: widgetFor('body')) but even once I've got this figured out, my template is still crashing. It turns out there are a couple of reasons - firstly, the date for my template is formatted by GraphQL, but from the editor it's being passed through as an object. I'll need to format it to a string for my template to render it. Secondly, my template expects htmlAst to be passed through, but this is prepared by Gatsby. How, when and where this happens I am not entirely sure.

The date being passed through is a Date object, which I can convert to a string using date.toLocaleDateString("en"). This produces a numeric date, but with some additional options I can get the date to display the same as on my page.

  const postDate = entry
    .getIn(["data", "date"])
    .toLocaleDateString("en", {
      year: "numeric",
      month: "long",
      day: "numeric",
    });

To convert the post content to htmlAst is another matter... AST stands for Abstract Syntax Tree (in case you were wondering) and can used to represent the syntactic layout of code in any programming language. I do some digging in Gatsby's own gatsby-transformer-remark and manage to figure out that Remark is doing most of the work in converting Markdown to AST. I can also see that Gatsby uses mdast-util-to-hast to convert the AST of Markdown into a corresponding AST of HTML. Doing my best to replicate Gatsby's options for Remark, my preview module now looks like this:

import * as React from "react";
import Remark from "remark";
import toHAST from "mdast-util-to-hast";

import { BlogTemplate } from "../../templates/blog-post.js";

// Setup Remark.
const remarkOptions = {
  commonmark: true,
  footnotes: true,
  gfm: true,
  pedantic: true,
  tableOfContents: {
    heading: null,
    maxDepth: 6,
  },
};
let remark = new Remark().data(`settings`, remarkOptions);

export default ({ entry, widgetFor }) => {
  const postDate = entry.getIn(["data", "date"]).toLocaleDateString("en", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
  const markdown = widgetFor("body").props.value;
  const markdownAst = remark.parse(markdown);
  const htmlAst = toHAST(markdownAst);
  return (
    <div className="avenir w-100">
      <BlogTemplate
        date={postDate}
        title={entry.getIn(["data", "title"])}
        description={entry.getIn(["data", "description"])}
        tags={entry.getIn(["data", "tags"])}
        content={htmlAst}
      />
    </div>
  );
};

There was no need to npm install Remark or mdast-util-to-hast because these are already dependencies of gatsby-transformer-remark. I now have a live preview of my blog post in the editor window. Amazing! One caveat - I can see that Gatsby's Remark transformer is careful to avoid multiple parallel conversions from Markdown to AST. As my preview function will be dealing with a constantly changing input, I wonder if it will cope with this, or will rapidly get overloaded. I'll have to try authoring a post in my CMS and see!

I have to make a few changes to my blogPreview component to deal with each field possibly being undefined when I create a new post. Once this is done, I can create a new post and see the resulting rendered content live! Much like HackMD's side-by-side view, I can write in markdown and see the results rendered next to it. The only thing missing is the code blocks, even though I made sure to put the useEffect hook that calls PrismJS within the template component. I'm struggling to find an ideal solution - it would be best if I had a React component that could render code with highlighting instead of relying on a side effect to apply it to the whole page. For now, I'll settle for basic code block styling by adding a language-undefined class to the <pre> elements rendered by the renderAst function. Now even if Prism does nothing, the Prism CSS will render the code block fairly nicely, and when Prism is working properly on my site it will colour the syntax nicely too.

Rendering a blog page

Now I've got blog posts, I need a page at /blog to show them to the world. This page needs to render a list of my posts, in descending date order, with a neat preview of each post. I'm going to save myself from thinking too much about this and use a Tachyons article list component. In /pages/blog.js I make a page query to pull out all my blog posts (filtering with the template key again) and the relevant fields for the article list. I make sure to use GraphQL to format the date nicely using date(formatString: "MMMM DD, YYYY"). Then I pass all the blog posts through to a BlogRoll component that will render a list of posts for me I need a template to display each individual post, and a container that will hold all these items.

I copy the html elements and classes from the Tachyons component linked above, and make a few minor changes (I'd prefer to use <ul> and <li> to render a list as this seems more semantic, and use a <time> element for the date). With a little tweaking of the style classes I've got a working Blog page!

A couple of things are bothering me though... I've linked to my blog posts at /blog/${slug} but they don't seem to be there, just a blank white page. Very strange. A visit to Gatsby's 404 page by typing in a nonexistent path shows me the pages have been created without /blog at the front of their path. I add it to the path in gatsby-node.js and all is well. The other thing that's causing me a little grief is that I don't have alt text to pass to the image in each blog post in the list. For good accessibility, I should add an alt text field in my CMS for the featured image and pull that out to use in my blog list.

Takeaways

  • Setting up the CMS is pretty straightforward
  • Rendering the markdown content of a post is not
  • But, happily, markdown can be rendered into React components using the HTML AST provided by gatsby-transformer-remark