Converting Blog from Ghost to React-Static

13 February 2018By Rich @Black Sand Solutions
  • ReactJS
  • Static
  • Ghost
Photo by Jeremy Thomas on Unsplash

How I converted my Ghost blog to a static ReactJS based blog built with react-static

Converting Blog from Ghost to React-Static

Motivation

Last year I updated my blog to be a static version of my existing hosted Ghost blog. You can read about that here. This worked, but it was a little cumbersome - it required spinning up a local instance of Ghost and then scraping the content - HTML, CSS & JavaScript.

I've recently updated my portfolio site; converting it from a homespun gulp based static site, to a ReactJS based built with react-static. At the same time I gave the look and feel an overhaul; I'd like to have the same design in the blog.

Requirements

  • I want to continue editing posts in Markdown ala Ghost
  • Updating the blog with new content should be straightforward
  • It should be fast
  • The styling should be consistent with the main site

Steps

Import Existing Blog Posts

Currently the blog simply consists of a bunch of HTML, JS and CSS sitting in the public directory. (This is the scraped content mentioned above). Given that I want to edit posts in Markdown, the first step is to convert the exising content from HTML to Markdown.

To do this I wrote a simple script that uses Turndown to convert the HTML back into Markdown.

const turndownService = new Turndown()
//make sure we keep pre elements
turndownService.keep(['pre'])
const blogDataPath = path.resolve(__dirname, './public/blog')
const markdownArray = fs.readdirSync(blogDataPath)
  .filter(file => file.indexOf('.html') > -1)
  .map(file => {
    const markdown = fs.readFileSync(path.resolve(blogDataPath, file), 'utf8')
    const html = turndownService.turndown(markdown)
    return { file, html }
  })
// create new markdown files
const blogPath = path.resolve(__dirname, './blog')
markdownArray.forEach(item => {
  const filename = `${item.file.slice(0, -5)}.md`
  fs.writeFile(`${blogPath}/${filename}`, item.html, err => {
    if (err) {
      return console.log(err)
    }
    console.log(`${filename} saved`)
  })
})

I then opened each file and added some front matter.

title: 'Angular 2 Module Paths'
path: 'angular-2-module-paths'
tags: 'Angular'
banner: 'https://res.cloudinary.com/blacksandsolutions/image/upload/v1517180746/becausereasons/beach.jpg'
date: '8 February 2016'
lead: 'It took me a while to get my head around the module paths used when importing modules in Angular 2.'

Use the Markdown in React-static

The next step was to use this markdown to create the data for the react-static app.

In the getRoutes method of the static.config.js file...

    // configure the remark parser
    const remarkParser = remark().use(html)
    // point to our blog files
    const dirPath = path.resolve(__dirname, './blog')
    // read in each markdown file
    const contentArray = fs.readdirSync(dirPath)
      .filter(file => file.indexOf('.md') > -1)
      // for each file, extract the front matter and the file contents
      .map(file => {
        const { data, content } = matter(fs.readFileSync(path.resolve(dirPath, file), 'utf8'))
        const { contents } = remarkParser.processSync(content)
        return { data, contents }
      })
      // and return as an array sorted by post date
      .sort((a, b) => sortDateDescending(a, b))

This data is then provided to the routes associated with the blog.

  • The blog container gets the list of front matter, data.
  • Each post gets the front matter (data) and the contents; additionally we also pass the previous and next post.
      {
        path: '/blog',
        component: 'src/containers/Blog',
        getProps: () => ({
          title: 'Because Reasons',
          data: contentArray.map(({ data }) => data),
        }),
        children: contentArray.map(({ data, contents }, index, posts) => {
          const next = getNextPost(index, posts)
          const prev = getPreviousPost(index, posts)
          return {
            path: `/posts/${data.path}`,
            component: 'src/containers/Post',
            getProps: () => ({
              contents,
              data,
              next,
              prev,
            }) }
        },
        ),
      },

Display the post content

The content for each post is simply a huge string containing some HTML markup.

In order to display this with a component we need to use dangerouslySetInnerHTML

    <section className="wrapper">
       <div className="inner post" id="post-content">
            <Hero matter={this.props.data} />
            <p>{this.props.data.lead}</p>
            <div dangerouslySetInnerHTML={{ __html: this.props.contents }} />
       </div>
    </section>

Code Styling

With these changes I now have my blog running inside of my react-static app. There is one thing left to do however. My Ghost blog was set up to use Google's code-prettify to apply some styling to the code blocks.

Update to work with react-prettify

To achieve this I decided to use this React Component. The content for each post is HTML markup injected using dangerouslySetInnerHTML.

In our Markdown we display javascript like this

  ```javacript
    const whiteGuy = 'pretty fly for a'
    ```

or if it's HTML, like this

  ```html
    <div>we use this syntax as html does not display when using the '<pre>'</div>
    ```

or if CSS, like this

  ```css
    .darthVader { color: black; }
    ```

You get the idea...

We now need to update this markup and insert the above React component where appropriate.

The following code achieves that:

  • first we find all elements that have a language class - this implies they are code blocks
  • we then insert a wrapping element and give it an id
  • insert the react component within the wraper
  • finally we remove the old markup
    const codeSnippets = document.getElementById('post-content')
     .querySelectorAll('.language-html, .language-javascript, .language-css')
    Array.from(codeSnippets).forEach((element, idx) => {
      // create a new code wrapper element and assign it a unique id
      const codeWrapper = document.createElement('div')
      const id = `pretty${idx}`
      codeWrapper.setAttribute('id', id)
      this.insertAfter(element, codeWrapper)
      // then render our Code component in the new wrapper
      ReactDOM.render(
        html<Code codeString={element.innerHTML} />, document.querySelector(`#${id}`))
      // then remove the original pre element
      element.parentNode.removeChild(element)
    })

Updating Content

Now updating content is as simple as creating a new Markdown file, adding some front matter and then building the site. In fact if I was blase enough not to test locally, I could simply commit and push the changes after adding the markdown and the site would be built and deployed automagically by Netlify.

TODO

The blog is good enough to go live, but of course, development is never complete -there are always tweaks to be made! Currently I find all image tags and replace them with a new tag that contains a srcset. However, they all use the same sizes and breakpoints. The result is some images display much larger than is ideal. Next up i'll read in a required size from the markdown. I'd also like to replace the existing Load More button with infiinte scrolling and provide a way to view all posts by tag.

All Posts