From Wordpress to GatsbyJS

Migrating blog from Wordpress to GatsbyJS: a solution

Photo by cluttersnap on Unsplash

I personally don't like Wordpress because, as soon as you try to build something a bit more complex, you have to add plugins and it quickly becomes expensive.

The same goes for the workload. It may automate a lot of the work initially, but again, as soon as it grows, you need a developer to keep up with all those plugins.

GatsbyJS to the rescue! But even though one of the main drivers of Gatsby's development was to offer an alternative to Wordpress, migrating a blog is still not straightforward. There are some pitfalls along the way.

In this article you will see how I did, and the compromises I chose to make.

The final result looks like the website Mundo Século XXI, built by yours truly.

Contents

  1. Exporting from Wordpress
  2. Gatsby config
  3. Gatsby node
  4. Main page

    4.1. Frontmatter

    4.2. Images

Exporting from Wordpress

Wordpress allows the export of its content as an xml file. It is a good idea to convert that to markdown, so that we can use several Gatsby plugins that will do a lot of the heavy lifting for us.

Needless to say it here: unlike Wordpress, you don't have to pay for Gatsby plugins.

There are a few good options to make this conversion. Exitwp, for example, is a good effort in Python.

I chose Wordpress export to markdown because it does a great job retrieving the images, and it was written in NodeJS.

In my case, the blog posts I exported from Wordpress were divided into categories and tags, which is quite common. Any converter I found retrieved that information. So I forked the Wordpress-export-to-markdown repository and modified it.

I sent my changes as a pull request to the original repository, but as of now it was not merged yet. So, in case you want use that converter with my code for categories and tags, you can use my fork.

This converter allows you to choose how to organize your markdown files. I suggest separating posts by year and month, prefixing each post with the date.

Gatsby config

Once we have the posts in markdown format, it is time to play with Gatsby.

Here are the plugins I added:

  • gatsby-plugin-sharp
  • gatsby-transformer-sharp
  • gatsby-transformer-remark

    • gatsby-remark-images
    • gatsby-remark-copy-linked-files
  • gatsby-plugin-react-helmet
  • gatsby-source-filename: one config for each directory (blog and images)
  • gatsby-plugin-manifest
  • gatsby-plugin-less
  • gatsby-plugin-react-svg
gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Wordpress to Gatsby`,
    description: `Wordpress to Gatsby`,
    author: `@Rodrigo_plp`,
  },
  plugins: [
    `gatsby-plugin-sharp`,
    `gatsby-transformer-sharp`,
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-images',
            options: {
              maxWidth: 590
            }
          },
          `gatsby-remark-copy-linked-files`
        ]
      }
    },
    `gatsby-plugin-react-helmet`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `blog`,
        path: `${__dirname}/src/blog`,
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/images`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `gatsby-starter-default`,
        short_name: `starter`,
        start_url: `/`,
        background_color: `#663399`,
        theme_color: `#663399`,
        display: `minimal-ui`,
        icon: `src/images/icon.png`
      }
    },
    `gatsby-plugin-less`,
    {
      resolve: 'gatsby-plugin-react-svg',
      options: {
        rule: {
          include: /\.inline\.svg$/
        }
      }
    }
  ]
}

Gatsby node

This is where you tell Gatsby about the structure of the directories you exported from Wordpress, and which information you want to retrieve from it.

For the purposes of building a blog, make use the two actions creatPages and onCreateNodes.

In onCreateNodes there are two things you will want done: to retrieve the slugs of each post, and to create a timestamp for each of them.

The slugs will be used as a reference to build your pages.

The timestamp is to allow you to filter posts by date. Gatsby doesn't do it, so this is to create a string from the date that can be easily consumed from the frontmatter.

gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value
    })

    const date = node.frontmatter.date
    createNodeField({
      name: 'timestamp',
      node,
      // convert date to unix timestamp & convert to number
      value: +moment(date).format('X'),
    })
  }
}

In createPages you will provide the path to your blog post template, you will give a GraphQL query to retrieve the necessary data from your nodes, and create handlers to control the previous and next posts.

gatsby-node.js
exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage } = actions
  const blogPostTemplate = path.resolve(`src/templates/blog-template.js`)
  const result = await graphql(`
    {
      allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 1000
      ) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              title
              date
              categories
              tags
            }
          }
        }
      }
    }
  `)

  // Handle errors
  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  const posts = result.data.allMarkdownRemark.edges

  posts.forEach(({ node }, index) => {
    const next = index === 0 ? false : posts[index - 1].node
    const prev = index === posts.length - 1 ? false : posts[index + 1].node
    
    createPage({
      path: node.fields.slug,
      component: blogPostTemplate,
      context: {
        slug: node.fields.slug,
        prev,
        next
      },
    })
  })
}

You won't use gatsby-browser.js or gatbsy-ssr.js on this setup. They can be deleted.

Main page

The main page is build in src/index.js, and it holds a few challenges before accepting our Wordpress export.

Frontmatter

The easiest part is harvesting the information available in the frontmatter. It takes a GraphQL query like this:

src/pages/index.js
query MyQuery {
  complete: allMarkdownRemark(sort: {order: DESC, fields: [frontmatter___date]}, limit: 4) {
    edges {
      node {
        frontmatter {
          title
          date(
            formatString: "MMMM DD, YYYY"
            locale: "pt"
          )
          categories
          coverImage
          tags
          summary
        }
        excerpt(pruneLength: 120)
        fields {
          slug
        }
      }
    }
  }
}

This query has a name, "complete", that returns the 4 newest posts (set by limit: 4 on the second line), formats the date in Portuguese (with the super practical locale flag) and prunes the length of the excerpt in 120 characters.

That's a lot of bang for the buck!

You use the resulting data by allocating it into a variable (called "posts" in the example below) and mapping it:

src/pages/index.js
const IndexPage = ({ data }) => {
  const {
    complete: { edges: posts }
  } = data

  return (
    <div className='home'>
      <div className='ultimas'>
        {posts.map(({ node }) => {
          return (
            <div className='post-banner' key={node.frontmatter.title}>
              <Link
                style={{ boxShadow: `none` }}
                to={node.fields.slug}
              >
                <article>
                  <header>
                    <div className='post-image'>
                      {node.frontmatter.coverImage === null ?
                        (<div className='stock-image'><Image /></div>) :
                        (                          <div className='normal-image'>                            <Banners props={node.frontmatter.coverImage} />                          </div>                        )                      }
                    </div>
                    <h3>{node.frontmatter.title}</h3>
                  </header>
                  <section>
                    <div className='post-summary'>
                      <p
                        dangerouslySetInnerHTML={{
                          __html: node.frontmatter.summary || node.excerpt,
                        }}
                      />
                    </div>
                  </section>
                </article>
              </Link>
            </div>
          )
        })}
      </div>
    </div>
  )
}

Note the highlighted line in the code above. It brings up the issue with the images.

Images

The remark plugin makes it very easy to display images inside blog posts. You just include a reference to their relative path and that is it. But retrieving one image per post to display on your front page is another story.

Some people approach this by configuring the heck out of the MDX plugin, others artificially include variables in static queries. I personally found those solutions added way too much complexity to the architecture.

Inspired by those solutions, I chose a compromise that requires manual work, but keeps things simple.

Gatsby already has a straight forward manner to include images in JavaScript pages, which is writing a component that uses gatsby-image and a static query to load it.

scr/components/image.js
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"

const Image = () => {
  const data = useStaticQuery(graphql`
    query {
      placeholderImage: file(relativePath: { eq: "msxxi.png" }) {
        childImageSharp {
          fluid(maxWidth: 300) {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  `)

  return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
}

export default Image

You specify the name of the image in the placeholderImage field. Now, to do the same for all posts I use a routine that dynamically creates an image from the relativepath of the node.

src/components/banners.js
import React from "react"
import { StaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"

const Banners = ({ props }) => {
  return <StaticQuery
    query={graphql`
      query {
        images: allFile(filter: { sourceInstanceName: { eq: "images" } }) {
          edges {
            node {
              relativePath              childImageSharp {
                fluid(maxWidth: 1000) {
                  ...GatsbyImageSharpFluid
                }
              }
            }
          }
        }
      }
    `}
    render={(data) => {      const image = data.images.edges.find(        image => image.node.relativePath === props      )      
      if (image && image.node.childImageSharp !== null) {        return (<Img fluid={image.node.childImageSharp.fluid} />)      } else {        console.log('>>>>> path', props)        return null      }    }}  />
}

export default Banners

In order for this to work, I have to manually copy the images from the posts to a src/images folder. That is the compromise.

It works because, as you can see in the frontmatter section of this article above, I am displaying a limited amount of articles on the front page (4 articles). So I only have to copy 4 images. When I add new articles, I copy its image to that images folder.

Next, let's see how to separate articles per category on the front page. Coming soon.

Questions? Hit me on the comments.

If you like GatsbyJS as I do, you may enjoy my tutorial Use Express and NGINX with a reverse proxy to add a backend to Gatsby.

You should follow me on Twitter.

I'm Rodrigo Pinto. I create content with the purpose of providing value to you. You won't find ads or sponsored products here. If you enjoy my content, please consider supporting what I do.

© 2020, Rodrigo P. L. Pinto