Unlikenesses A Backend Developer

Migrating the blog from Jekyll to Gatsby

10 January 2018

Notes on converting this blog from a Jekyll-generated site (with the Hyde theme) to the React-based Gatsby.js, retaining the same theme. The source code can be found here.

Getting started

After installing Gatsby on my machine, the first step is to create a new project using its "hello world" scaffolding:

gatsby new blog-gatsby https://github.com/gatsbyjs/gatsby-starter-hello-world

The first thing I want to do is check I can display the posts I already have on the Jekyll blog, with its basic look-and-feel, leaving most of the functionality to later. To speed things up, I won't be using Gatsby's built-in CSS Modules; instead, I create a css folder in src and copy my CSS files there. Then I create a layouts folder in src, and create the basic layout, index.js:

import React from "react";
import Sidebar from "./sidebar";
import "../css/poole.css";
import "../css/hyde.css";

export default ({ children }) => (
  <div className="theme-base-08">
    <Sidebar />
    <div className="content container">{children()}</div>
  </div>
);

This uses the same mark-up as the Hyde theme. My sidebar component is, for now, a simplified version of the Hyde sidebar (I don't automatically generate any links there yet). See the repo for the final sidebar. To get the Google fonts loaded I install the Gatsby Google fonts module:

npm install gatsby-plugin-google-fonts --save

and put the details in the root gatsby-config.js file:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-google-fonts`,
      options: {
        fonts: [`Roboto+Slab\:700`, `Noto+Serif\:400,400i,700,700i`]
      }
    },
  ]
};

Displaying posts

The next step is to copy across the posts. Create a posts folder in src and copy the posts across from the Hyde _posts folder.

We'll need the gatsby-source-filesystem plugin for accessing the files, and the gatsby-transformer-remark plugin for parsing the markdown:

npm install --save gatsby-source-filesystem npm install --save gatsby-transformer-remark

Updated config file (with the site title):

module.exports = {
  siteMetadata: {
    title: `Unlikenesses`
  },
  plugins: [
    {
      resolve: `gatsby-plugin-google-fonts`,
      options: {
        fonts: [`Roboto+Slab\:700`, `Noto+Serif\:400,400i,700,700i`]
      }
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`
      }
    },
    `gatsby-transformer-remark`
  ]
};

Now we can pass the site title to the sidebar (<Sidebar title={data.site.siteMetadata.title} />) with this graphQl query:

export const query = graphql`
  query AboutQuery {
    site {
      siteMetadata {
        title
      }
    }
  }
`;

Now, the pages/index.js file just needs to grab the list of posts:

export const query = graphql`
  query IndexQuery {
    allMarkdownRemark {
      totalCount
      edges {
        node {
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          excerpt
        }
      }
    }
  }
`;

and map over them, outputting the same markup as the original blog:

export default ({ data }) => {
  console.log(data);
  return (
    <div className="posts">
      {data.allMarkdownRemark.edges.map(({ node }, idx) => (
        <div className="post" key={idx}>
          <h1 className="post-title">{node.frontmatter.title}</h1>
          <span className="post-date">{node.frontmatter.date}</span>
          <div dangerouslySetInnerHTML={{ __html: node.excerpt }} />
        </div>
      ))}
    </div>
  );
};

There's a problem with this though. The Hyde blog automatically takes the title and date of each post from the post's filename (unless otherwise specified in the front-matter). It'd be nice if we could do the same here. We'll cover that in the next section.

Linking to a post

At the moment we're only showing excerpts. To create and link to the actual posts we'll follow the official tutorial. So, create gatsby-node.js, and first, to create the slugs, paste in this code (taken more or less directly from the official docs):

const { createFilePath } = require("gatsby-source-filesystem");

exports.onCreateNode = ({ node, getNode, boundActionCreators }) => {
  const { createNodeField } = boundActionCreators;
  if (node.internal.type === "MarkdownRemark") {
    const slug = createFilePath({ node, getNode, basePath: "posts" });
    createNodeField({
      node,
      name: "slug",
      value: slug
    });
  }
};

This will add a slug field to the list of markdown pages. But while we're adding fields, why not add new fields which contain the title and date of a post based on its filename? Under the slug declaration, we can add a small bit of JavaScript to parse the slug into date and title:

let title = node.frontmatter.title;
let date = node.frontmatter.date;
if (title === "" || date === null) {
  let nameArr = slug.replace(/\//g, "").split("-");
  date = nameArr.splice(0, 3).join("-");
  title = nameArr.join(" ").replace(".md", "");
}

If the title or date are empty, we derive them from the slug. Remember the format of a slug is /2018-01-01-title-of-post.md/. First we remove the bounding / characters, then split it at its hyphens. Using splice we grab the first three elements of the resultant array, and join them up again to form the date. The title is the rest of the array, joined with spaces and with the final .md removed. Then we can create the new node fields:

createNodeField({
  node,
  name: "title",
  value: title
});
createNodeField({
  node,
  name: "date",
  value: moment(date).format("DD MMMM, YYYY")
});

I'm importing Moment.js at the top of the file, and using it here to format the date. It comes with Gatsby's node modules so there's no need to install it separately. (NB. You'll need to stop and restart the Gatsby server.)

Now we return to pages/index.js. Import Link at the top:

import Link from "gatsby-link";

Now we can replace the h1 tag with

<Link to={node.fields.slug} className="post-title">
  {node.fields.title}
</Link>

Notice here we're pulling the slug and title fields from the node. We can do the same with the date:

<span className="post-date">{node.fields.date}</span>

To pull in this data we just need to modify the GraphQL query to include the new fields:

node {
  id
  fileAbsolutePath
  frontmatter {
    title
    date(formatString: "DD MMMM, YYYY")
  }
  fields {
    slug
    title
    date
  }
  excerpt
}

Now we need to create the pages these Link tags point to. Back in gatsby-node.js add the createPages function (taken from the official docs):

exports.createPages = ({ graphql, boundActionCreators }) => {
  const { createPage } = boundActionCreators;
  return new Promise((resolve, reject) => {
    graphql(`
      {
        allMarkdownRemark {
          edges {
            node {
              fields {
                slug
              }
            }
          }
        }
      }
    `).then(result => {
      result.data.allMarkdownRemark.edges.map(({ node }) => {
        createPage({
          path: node.fields.slug,
          component: path.resolve("./src/templates/post.js"),
          context: {
            // Data passed to context is available in page queries as GraphQL variables.
            slug: node.fields.slug
          }
        });
      });
      resolve();
    });
  });
};

The only change I've made is to modify the location and filename of the individual post component. We can create that now. The React function just renders the HTML:

export default ({ data }) => {
  const post = data.markdownRemark;
  return (
    <div className="post">
      <h1 className="post-title">{post.fields.title}</h1>
      <span className="post-date">{post.fields.date}</span>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </div>
  );
};

and the GraphQL query pulls in the appropriate data, based on the slug passed to it:

export const query = graphql`
  query BlogPostQuery($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      fields {
        title
        date
      }
    }
  }
`;

Ordering Posts

To order the posts by date (which amounts to ordering them by filename), we add a sort statement to the GraphQL query:

allMarkdownRemark(sort: {fields: [fileAbsolutePath], order: DESC}) {

One other detail: the Hyde blog hides (ha ha) all posts which have published: false in their front-matter. To replicate this we can add a simple filter to the query:

allMarkdownRemark(sort: {fields: [fileAbsolutePath], order: DESC}, filter:{frontmatter: {published: {eq: true}}}) {

Let's also add a filter to grab only pages of the type "post" - we'll need this later:

allMarkdownRemark(sort: {fields: [fileAbsolutePath], order: DESC}, filter:{frontmatter: {published: {eq: true}, layout: {eq: "post"}}}) {

Pagination

The Hyde blog has "Newer" and "Older" links at the bottom of each page, with the pagination value being set in its _config.yml file. Pagination isn't supported out of the box in Gatsby at the time of writing, but there is this module, which looks promising. Following the instructions, first install the package: npm install gatsby-paginate --save. Then require it at the top of gatsby-node.js:

const createPaginatedPages = require('gatsby-paginate');

Then call createPaginatedPages before the createPage function:

createPaginatedPages({
  edges: result.data.allMarkdownRemark.edges,
  createPage: createPage,
  pageTemplate: "src/templates/index.js",
  pageLength: 3
});

Next we need to modify the createPages GraphQL query to match the one in pages/index.js, and finally create src/templates/index.js. This will be a combination of the example function from the docs and the markup from pages/index.js, with some extra pagination mark-up from the old Hyde blog:

const NavLink = props => {
  if (!props.test) {
    return <Link to={props.url} className="pagination-item">{props.text}</Link>;
  } else {
    return <span className="pagination-item">{props.text}</span>;
  }
};

export default ({data, pathContext}) => {
  const { group, index, first, last } = pathContext;
  const previousUrl = index - 1 == 1 ? "" : (index - 1).toString();
  const nextUrl = (index + 1).toString();

  return (
    <div>
      {group.map(({ node }, idx) => (
        <div className="post" key={idx}>
          <Link to={node.fields.slug} className="post-title">
            {node.fields.title}
          </Link>
          <span className="post-date">{node.fields.date}</span>
          <div dangerouslySetInnerHTML={{ __html: node.excerpt }} />
          <Link to={node.fields.slug}>Read More ></Link>
        </div>
      ))}
      <div className="pagination">
        <NavLink test={last} url={nextUrl} text="Older" />
        <NavLink test={first} url={previousUrl} text="Newer" />
      </div>
    </div>
  );
}

This should be mostly self-explanatory - take a look at the pagination package docs if you want to find out more. Don't forget to delete pages/index.js.

Syntax highlighting

We're still missing syntax highlighting - luckily there's a plugin for this: npm install --save gatsby-remark-prismjs. Gatsby-remark-prismjs uses PrismJS to add syntax highlighting to markdown files. After it's installed we add it as a plugin in the options for gatsby-transformer-remark, ingatsby-config.js:

{
  resolve: `gatsby-transformer-remark`,
  options: {
    plugins: [
      {
        resolve: `gatsby-remark-prismjs`,
      }
    ]
  }

Then import the CSS in layouts/index.js:

import "prismjs/themes/prism-tomorrow.css";

Other pages

We're almost there. In my blog there's a couple of static pages which are listed in the sidebar. I'll put those in the pages folder of the Gatsby blog, which should currently be empty. Each of these pages should have some front-matter: a "layout" (set to "page"), a "published" setting, and a "title". E.g.:

---
title: About
layout: page
published: true
---

Now we'll need to modify gatsby-node.js. First, since these pages are in a different folder, we have to set that in the basePath:

let basePath = "posts";
if (node.frontmatter.layout === "page") {
  basePath = "pages";
}
const slug = createFilePath({ node, getNode, basePath });

We're just checking to see if the page has a layout attribute of "page", and if so, we alter the basePath variable and pass it in when creating the slug. We also need to check for the layout when creating the date node, since we don't need it in this case:

if (node.frontmatter.layout !== "page") {
  createNodeField({
    node,
    name: "date",
    value: moment(date).format("DD MMMM, YYYY")
  });
}

Now currently the GraphQL query for createPages gets both the posts and the pages. We need to make two queries, one for the posts, one for the pages. we need some extra info when creating the pages. So give the "posts" query a name:

posts: allMarkdownRemark(

(This means we need to rename the object of the createPaginatedPages and map methods underneath, from allMarkdownRemark to posts.) Then under that create a new query called "pages":

pages: allMarkdownRemark(
  sort: { fields: [fileAbsolutePath], order: DESC }
  filter: { frontmatter: { published: { eq: true }, layout: { eq: "page" } } }
) {
  edges {
    node {
      fields {
        title
        slug
      }
    }
  }
}

This is almost exactly the same as "posts", except that it filters for pages with the "page" layout attribute in the front matter. The final step here is to map over the results of the query and create the pages, passing a different template, page.js:

result.data.pages.edges.map(({ node }) => {
  createPage({
    path: node.fields.slug,
    component: path.resolve("./src/templates/page.js")
  });
});

Now we have the pages we can link to them in our sidebar. I make the query in layouts/index.js and pass the result to the sidebar component:

allMarkdownRemark(
  sort: { fields:  [frontmatter___title]}
  filter: { frontmatter: { layout: { eq: "page" } } }
) {
  edges {
    node {
      fields {
        title
        slug
      }
    }
  }
}

With the filter I only get the pages with a "page" layout. Then I pass them to the sidebar component:

<Sidebar
  title={data.site.siteMetadata.title}
  description={data.site.siteMetadata.description}
  pages={data.allMarkdownRemark.edges}
/>

In Sidebar.js I just need to map over them and display a link:

{pages.map((page, idx) => {
  return (
    <Link
      className="sidebar-nav-item"
      to={page.node.fields.slug}
      key={idx}
    >
      {page.node.fields.title}
    </Link>
  );
})}

The final step is to create the page template, templates/page.js:

export default ({ data }) => {
  const page = data.markdownRemark;
  return (
    <div>
      <h1 className="page-title">{page.fields.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: page.html}} />
    </div>
  );
};

export const query = graphql`
  query PageQuery($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      fields {
        title
      }
    }
  }
`;

It's basically a much simpler version of post.js, since we only want the title and html fields.

Related Posts

Since what Jekyll calls "Related Posts" is actually just "Recent Posts", I decided to ditch it in favour of "previous / next post" links. We can get the previous and next posts in our GraphQL query in gatsby-node.js. (Much of this is derived from Ian Sinnott's gatsby-node.js):

next{
  fields {
    title
    slug
  }
}
previous {
  fields {
    title
    slug
  }
}

and pass them in the context (swapping them around, since "next" gives us an older post):

createPage({
  path: node.fields.slug,
  component: path.resolve("./src/templates/post.js"),
  context: {
    slug: node.fields.slug,
    prev: next,
    next: previous
  }
});

Now we have the previous and next results we can display the links accordingly in post.js:

<div className="related">
  {pathContext.prev ? (
    <div>
      <h3>
        Previous:{" "}
        <Link to={pathContext.prev.fields.slug}>
          {pathContext.prev.fields.title}
        </Link>
      </h3>
    </div>
  ) : null}
  {pathContext.next ? (
    <div>
      <h3>
        Next:{" "}
        <Link to={pathContext.next.fields.slug}>
          {pathContext.next.fields.title}
        </Link>
      </h3>
    </div>
  ) : null}
</div>

Full source code.