Photo by Filiberto Santillán on Unsplash
In this article, we will go through step-by-step how you can build a Gatsby static site that uses Ghost as a headless CMS. This way you can get the best of both worlds: the great writing experience of Ghost and all of the performance benefits of a Gatsby static-site.
You will learn how to set up both Gatsby and Ghost on your local computer, you will see how to build Gatsby templates using data sourced from Ghost, and finally see how to deploy your site on Netlify so that it refreshes every time you change your content.
You can find all of the finished code for the project here.
Before we get started, let’s do a quick summary of the tools we’ll be using.
Ghost is a free and open-source blogging platform that was created as a more simple alternative to WordPress. The founder of Ghost, John O’Nolan, believed that WordPress had become too bloated with features and the goal of Ghost was to create a more streamlined experience that focused on writing.
It was also a chance to reimagine a CMS built on more modern technologies, and as a result, Ghost was built entirely in Node.js. Apart from its core features for hosting a blog, Ghost has a wide range of easy-to-use API’s which makes it ideal as a headless CMS.
Gatsby JS is a free and open-source front-end framework that uses React and GraphQL to generate static sites.
Like other static site generators such as Next JS and Jekyll, it does a lot of the hard work around performance optimization for you. It also has a wide range of plugins to help manage things like SEO.
Netlify is a platform for deploying static sites that has a powerful range of features that make the build process simple. They have a generous free tier that we will be taking advantage of to deploy and host our finished static site.
Let’s get started by installing Ghost locally on your computer. There are full instructions with prerequisites over on the Ghost site here, but in short, it is a simple two-step process:
Install the Ghost command-line tool Ghost-CLI:
npm install ghost-cli@latest -g
And then you can run the following command in whichever directory you would like to install Ghost in:
ghost install local
Follow the instructions through their self-guided install and you’ll be able to access your Ghost Admin panel at http://localhost:2368/ghost.
From there, we will log in to the Admin panel. (it will prompt you to do some basic set-up like creating your name and password)
Once we are in the Admin back-end we will first disable Ghost from serving the front-end of the site itself. (since we will be doing this with Gatsby instead) The Ghost team has made this easy to do, you just need to go to Settings > General and you will see an option to “Make this site private.”
This will password protect the site and add meta tags that will prevent all of the content from being indexed by search engines. (again, since we want our Gatsby site to be indexed rather than our Ghost site)
Finally, we just have to add an API integration that we will use to send our content to our Gatsby site. You can do that by going to Integrations > Add custom integration. You can call this new integration Gatsby and then you will want to take note of the Content API key and the API Url.
Now with that done, we’re ready to move over to our Gatsby build.
Now we’re ready to build our Gatsby site that will consume the content from our Ghost API.
We will start by installing the Gatsby CLI (Command Line Interface). To do this, you will simply run the command:
npm install -g gatsby-cli
Next, we will scaffold our new Gatsby site from a starter template. For this tutorial, we will use the Gatsby Starter Blog. So navigate to the folder you want to install in and run:
gatsby new <your-project-name> https://github.com/gatsbyjs/gatsby-starter-blog
One of the best parts about Gatsby is it makes development super easy - all you need to do is run one command and you have a development server running that hot-reloads all of your changes. To do that, we will just navigate into your project’s folder and run:
gatsby develop
You can now open up http://localhost:8000/ in your browser and you will see your site running.
Now that our Gatsby site is up and running our next step will be connecting it to our Ghost API.
Typically when you are building a blog in Gatsby, all of the content will load from markdown files that you keep in the content folder of your project. If you see the file structure of your project, you will notice it looks like this:
src
└ content
├ hello-world
├ my-second-post
└ new-beginnings
You can go ahead and delete this content folder, as we won’t need it going forward.
Now we need to install a Gatsby plugin called gatsby-source-ghost, which you will do with the command:
npm install --save gatsby-source-ghost
Then you will go into your gatsby-config.js file and make the following addition to your plugins array:
{
resolve: `gatsby-source-ghost`,
options: {
apiUrl: `<Your API URL>`,
contentApiKey: `<Your Content API>`,
}
}
This is telling Gatsby that it can source its content from Ghost and how it should connect to the API. With that in place, you can now remove these items from your plugins array in the gatsby-config.js file:
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/content/blog`,
name: `blog`,
}
}
Next, we have to go into our gatsby-node.js file. This is where we tell Gatsby what to do with our Ghost data and how to actually build our site.
There a few different changes here, so I will give you all of the revised code and up-front explain what’s going on after:
const path = require(`path`)
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
{
allGhostPost(sort: { order: ASC, fields: published_at }) {
edges {
node {
slug
}
}
}
}
`)
// Check for any errors
if (result.errors) {
throw new Error(result.errors)
}
// Extract query results
const posts = result.data.allGhostPost.edges
// Load templates
const postTemplate = path.resolve(`./src/templates/blog-post.js`)
// Create post pages
posts.forEach(({ node }) => {
// This part here defines, that our posts will use
// a `/:slug/` permalink.
node.url = `/${node.slug}/`
createPage({
path: node.url,
component: postTemplate,
context: {
// Data passed to context is available
// in page queries as GraphQL variables.
slug: node.slug,
},
})
})
}
In our allGhostPost GraphQL query we are specifying what data we need from our headless Ghost instance at the time our Gatsby site is building. You can see it only needs the slug of each post as this is the information required to generate the url of each of our Gatsby pages. The rest of the information from Ghost will be queried later on in the template that is used to generate the individual posts.
You can then see that we are specifying that template as blog-post.js, which we will be updating in a bit. We are mapping through all of the posts from our Ghost query to create a new page from that template that has a url path according to the slug we queried from Ghost. Finally, you will see that we are passing the slug itself into the context of the page so we can access that through our individual GraphQL queries later.
Now that we have all of our gatsby-node.js file updated, we will just need to update the code in our index.js file and our blog-post.js template files so that our Ghost data flows through correctly to our templates.
Let’s start in our index.js file with the Graphql query at the bottom. Currently, it looks like this:
export const pageQuery = graphql`
query {
site {
siteMetadata {
title
}
}
allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
nodes {
excerpt
fields {
slug
}
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
description
}
}
}
}
`
We will need to replace the allMarkdownRemark section, as this query is translating our previous markdown content. Our new query will look like this:
export const pageQuery = graphql`
query {
site {
siteMetadata {
title
}
}
allGhostPost(sort: { fields: [published_at], order: DESC }) {
edges {
node {
id
title
slug
excerpt
published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
}
}
}
}
`
Remember we are on our index.js page, so we want to get all of our posts in order to list them. For that, we want the id, title, slug, excerpt, and date published.
Now that we have our query updated, we have to go through the file and make some quick changes since our new data query is slightly different than our previous one.
First, we will change:
const posts = data.allMarkdownRemark.nodes
To:
const posts = data.allGhostPost.edges
Then we will need to check all of our template data references to make sure they agree. So for example, any place that we were referencing:
post.frontmatter.title
We should change to:
post.node.title
And instead of:
post.fields.slug
post.frontmatter.date
post.frontmatter.description
We will use:
post.node.slug
post.node.published_at_pretty
post.node.excerpt
After that, we will just need to do the same exact process for our blog-post.js template.
We’ll start with our query again, where our version will look like this:
export const pageQuery = graphql`
query($slug: String!) {
site {
siteMetadata {
title
}
}
ghostPost(slug: { eq: $slug }) {
id
title
slug
excerpt
published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
html
}
}
`
Here it looks pretty similar to our other query with the addition of html, which will be the actual body of our post. Also, we are filtering on slug to bring back the data for just one post, rather than pulling all of our posts like we were before.
Again we will have to go through and update our template data references. So we will replace this:
const post = data.markdownRemark
post.frontmatter.title
post.frontmatter.description
post.frontmatter.date
With:
const post = data.ghostPost
post.title
post.excerpt
post.published_at_pretty
And just one quick note on this file, we are deleting the code that would add next and previous tabs to each blog post since it would be a bit of a detour to explain - so we won’t worry about it for the purposes of this post.
Then you should be able to run gatsby develop again and see our content is now coming from Ghost!
As a final touch, you will see that the images in the blog posts are way too big, so we will just need to add this to our styles.css:
img {
max-width: 100%;
}
And that’s it for our Gatsby build.
Now that we have our Gatsby site ready to go, we can deploy it into production using Netlify. (we will assume that you already have an account, but if you don’t, you can create one for free here).
With Netlify, you have the option of either building locally with their command-line tool or building directly from Github. For the purposes of this tutorial, we will go with a local build since we are running Ghost locally. But a bit later I will also show you how you can build from Github and use Ghost’s webhooks to re-build whenever your content changes.
So to get started with Netlify’s command-line tool, you first need to install it:
sudo npm -g install netlify-cli
Now with that done, you can connect to your Netlify account by running:
netlify login
Then you need to first build your site for production, so from your project’s folder you will run:
gatsby build
You can check everything looks ok by running:
gatsby serve
This will preview your production-ready site at localhost:9000. Now if everything looks good, you can then run:
netlify deploy --prod
You will be prompted with a few options questions from Netlify that you can leave blank for now. Then you can go to the Netlify dashboard and see your site-building.
Once it has completed, you will see a url that has some random words that end in netlify.app. You can now view your site live at this url.
This is great for our tutorial, but the only thing we are missing is the ability to automatically refresh our content whenever it changes. This is because our content is being served from an API that is only available on our local computer.
So we would have to run gatsby build and netlify deploy --prod every time we update our content. This might be perfectly fine for a solo-project, but for something more heavyweight, you will want to host your Ghost instance online so the API can be accessible from Netlify. You can do this with Digital Ocean or AWS as well as most other cloud providers.
Then instead of deploying from Netlify locally, you would build directly from Github. Once you’ve pushed your production-ready site to Github you can go into the Netlify dashboard and select “New site from Git”.
Then just follow the instructions and leave all of the options as they are.
With this setup, we can then tell Netlify to re-build our site every time that our Ghost content changes. This is simple to do using Ghost’s webhooks and they have a great guide that shows how to do it here.
I won’t repeat the guide fully since it is pretty clear, but here’s the gist of what we need to do:
Then we will go to the Webhooks section and say Add Webhook - let’s call that Netlify Build
Paste in your URL we copied from Netlify earlier and select Site changed (rebuild)
Once this is all set up, every time you change your content, Ghost will fire off a webhook to your Netlify API URL notifying your site to rebuild.
And that’s it!
With that, you have seen how to build a fully functioning, blazing-fast blog built with Ghost and Gatsby with either local or continuous deploys from Netlify.
In this post, you learned how to:
This post just scratches the surface of what you can do with Ghost and Gatsby together. I’ve linked to some helpful resources at the end of the post that shows you how you can take things even further.
I hope you found this article to be a helpful introduction to using Ghost with Gatsby!
You can get even more ideas of what you can do with them together by checking out my project Epilocal where I am building technology for local news and other small online publishers using this same tech stack.
Gatsby Starter Ghost (demo) - the official starter for Gatsby from the Ghost team. They have some great resources for making your site more production-ready, like recommended static files for Netlify and structured data for enhanced SEO.
Newliner Gatsby + Ghost (demo) - a Gatsby + Ghost starter that uses Netlify serverless functions to interact with your headless Ghost instance to capture newsletter sign-ups.