Skip to main content
Sheelah Brennan

From WordPress to Gatsby: The Migration Back to a Static Site

In the last few years, there have been major advancements in static site generators, along with the growing popularity of the JAM (JavaScript, APIs, and markup) stack. I've had the itch to move my site back to a static site for awhile, closer to its Jekyll roots, and I finally settled on Gatsby. I worked on a Gatsby-based project last year and knew it would work perfectly for a blog.

Gatsby is a React-based static site generator that provides a nice developer experience and has a very active plugin community. While it's completely possible to set up an existing WordPress site as a content source for a Gatsby site (and still rely on WordPress for all content), I decided to fully migrate my site to Gatsby and store blog posts in markdown files and migrate over all images as well.

Blog Post Migration

I read about a neat tool meant for Hugo CMS that exports WordPress posts into markdown files in this blog post by Peter Akkies. I was able to run that tool and get my blog posts in the proper format for the most part. After running the tool, I had a folder full of blog posts that included dates in the filenames (Ex. 2017-06-26-unsplash-random-image-how-to.md). Since I wanted to keep my post URLs unchanged (good for SEO reasons too), I ran this to remove the dates from the post filenames:

  cd posts
  for x in `ls`; do n=`echo $x | cut -d”-” -f4-`; mv $x $n; done`

After that I had the posts filenames as I wanted (Ex. unsplash-random-image-how-to.md).

Blog Post File Structure

I decided to break out each of my blog posts into their own directory so that they'd be easier to manage. Another benefit of this is that each post's featured image and any other images included in the post could live in the same directory. So I created a directory for each post based on the post's title and placed the blog post inside that directory, naming it index.md.

I got this idea from Vojtech Ruzicka's Gatsby project on Github.

Image Attribution

With markdown as the source of my blog posts, I needed a way to add image attributions for featured images directly in markdown. I used the gatsby-remark-custom-blocks plugin for this, setting up a custom block for image attributions. This way I was able to add attributions to each post and also have a CSS class added in to allow for custom styling. I set a block up for blog post updates too. Here's how my gatsby-config.js was set for that:

{
  resolve: `gatsby-remark-custom-blocks`,
  options: {
    blocks: {
      postUpdate: {
        classes: `post-update`,
        title: `required`,
      },
      featuredImageAttribution: {
        classes: `featured_image_attribution`,
      },
    },
  },
},

And within a post I used the featured image attribution block like this:

[[featuredImageAttribution]]
| Featured image by [Tim Evans](https://unsplash.com/@tjevans) via Unspash

Tag and Category Archives

I wanted to keep my tag and category indexes around, similar to what's provided out of the box in WordPress. Luckily there's a lot of examples of how to set this up with Gatsby, including in the Gatsby docs. Here's the relevant part of my gatsby-node.js file. With this in place, categories or tags fields set inside blog posts' markdown files are processed as taxonomy data:

exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions

  const blogListTemplate = path.resolve(`./src/templates/blog-list.js`)
  const blogPostTemplate = path.resolve(`./src/templates/blog-post.js`)
  const tagPostTemplate = path.resolve(`./src/templates/tag.js`)
  const categoryPostTemplate = path.resolve(`./src/templates/category.js`)

  const tagPage = path.resolve(`./src/pages/tag.js`)
  const categoryPage = path.resolve(`./src/pages/category.js`)

  return graphql(`
    {
      allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 1000
      ) {
        edges {
          node {
            frontmatter {
              url
              categories
              tags
              title
            }
          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      return Promise.reject(result.errors)
    }

    const posts = result.data.allMarkdownRemark.edges

    // Prep categories and tags lists
    const postsByTag = {}
    const postsByCategory = {}

    posts.forEach(({ node }) => {
      if (node.frontmatter.tags) {
        node.frontmatter.tags.forEach(tag => {
          if (!postsByTag[tag]) {
            postsByTag[tag] = []
          }
          postsByTag[tag].push(node)
        })
      }

      if (node.frontmatter.categories) {
        node.frontmatter.categories.forEach(category => {
          if (!postsByCategory[category]) {
            postsByCategory[category] = []
          }
          postsByCategory[category].push(node)
        })
      }
    })

    const tags = Object.keys(postsByTag)
    const categories = Object.keys(postsByCategory)

    // Create tags index
    createPage({
      path: "/blog/tag",
      component: tagPage,
      context: {
        tags: tags.sort(),
      },
    })

    // Create individual tag indexes
    tags.forEach(tag => {
      const posts = postsByTag[tag]
      const tagUrl = tag.split(" ").join("-")
      const path = `/blog/tag/${tagUrl}/`

      createPage({
        path,
        component: tagPostTemplate,
        context: {
          tag,
          url: path,
        },
      })
    })

    // Create categories index
    createPage({
      path: "/blog/category",
      component: categoryPage,
      context: {
        categories: categories.sort(),
      },
    })

    // Create individual category indexes
    categories.forEach(category => {
      const posts = postsByCategory[category]
      const categoryUrl = category.split(" ").join("-")
      const path = `/blog/category/${categoryUrl}/`

      createPage({
        path,
        component: categoryPostTemplate,
        context: {
          category,
          url: path,
        },
      })
    })

SEO

Since I wasn't going to have access to WordPress plugins like Yoast to handle SEO (search engine optimization) basics for me, I knew I had some work to do on that front. I still wanted to set page titles and page-specific open graph meta tags for SEO. These affect things like what one of my blog posts looks like when it's shared on Twitter. Luckily the Gatsby docs include a guide to creating an SEO component, and I built something very similar to that.

RSS

I still like RSS, though admittedly I have too many of them in my feed reader which means I'm eternally behind. I still wanted to generate an RSS feed for my site, which is another thing that WordPress did for me. I found Gatsby's docs on adding an RSS feed using the gatsby-plugin-feed plugin pretty useful, and it didn't take much work to get that working.

The only issue I ran into is my Google Web Console was showing a "URL not allowed" error related to my RSS feed. I discovered that a link tag was being included by default linking to the underlying node library's URL instead of my site's URL. That was easy to fix: I just had to make sure to specify my site's URL in the site metadata passed in to the plugin. Here's what my gatsby-config.js ended up looking like for the RSS feed setup:

 {
      resolve: `gatsby-plugin-feed`,
      options: {
        query: `
          {
            site {
              siteMetadata {
                title
                description
                site_url: url
              }
            }
          }
        `,
        setup: ({
          query: {
            site: { siteMetadata },
            ...rest
          },
        }) => {
          return {
            ...siteMetadata,
            ...rest,
          }
        },
        feeds: [
          {
            serialize: ({ query: { site, allMarkdownRemark } }) => {
              return allMarkdownRemark.edges.map(edge => {
                return Object.assign({}, edge.node.frontmatter, {
                  description: edge.node.excerpt,
                  categories: edge.node.frontmatter.categories,
                  url: site.siteMetadata.url + edge.node.fields.slug,
                  guid: site.siteMetadata.url + edge.node.fields.slug,
                })
              })
            },
            query: `
            {
              allMarkdownRemark(
                sort: { order: DESC, fields: [frontmatter___date] },
              ) {
                edges {
                  node {
                    html
                    fields { slug }
                    excerpt
                    frontmatter {
                      title
                      date
                      categories
                      featured_image {
                        childImageSharp {
                            fluid {
                              originalImg
                            }
                        }
                      }
                    }
                  }
                }
              }
            }
          `,
            output: "/feed.xml",
          },
        ],
      },

Syntax Highlighting

For code syntax highlighting, I discovered gatsby-remark-prismjs, which is based, as you might guess, on PrismJS. I set up the theme I wanted by adding this tiny snippet to gatsby-browser.js:

require("prismjs/themes/prism-tomorrow.css")

Deployment

I decided to go with Netlify for my new site since they provide HTTPS and static site hosting for free. That's hard to beat! Another nice feature is that anytime I open a Github pull request, Netlify automatically runs a build and generates a live demo URL for that PR. Pretty useful :)

Issues Along the Way

Like anything new, there were a couple of problems that cropped up during my WordPress to Gatsby migration project. One is still unresolved and is annoying but not enough to make me abandon Gatsby:

  1. SSR wasn't fully working for my site. Running off a local build, I turned off JavaScript and was surprised to see that my site wasn't fully loading. This one ended up being my fault. I had forgotten that browser-specific features like local storage aren't available under SSR, so my site's dark mode was broken. Since that was at the time linked with setting a theme context for the site, nothing rendered fully with JavaScript disabled. Fortunately, the fix was easy. I just needed to add a check for window being undefined before accessing any browser-specific features, as discussed in this Github issue comment.
  2. Builds sometime trigger Sharp-related errors like (sharp:13989): GLib-CRITICAL **: 19:47:45.697: g_hash_table_lookup: assertion 'hash_table != NULL' failed. Sometimes these trigger a core dump and break the build entirely. I see these on both my local machine and on Netlify. Sharp is the image optimization node module that I'm using via gatsby-transformer-sharp and gatsby-plugin-sharp. I haven't found a silver bullet for this one. I do have some images for my site, but not hundreds, so I wouldn't expect to have any issues. This Github issue has more details.