Blogging with Gatsby and headless Ghost CMS

I changed my blog from using Hugo to Gatsby with a headless Ghost CMS. Learn about my experiences with this hosted on Cloudflare Workers.

Sat Feb 15 2020 - 11 min read

I am always on the hunt for the next improvement for my blog. I've been using Hugo and while I was happy with it did have some things that I did not enjoy as much.  We are using Gatsby with a headless CMS for a new project at work. Because of this I decided that I should give this a go to learn more. Since Ghost is pushing the idea about it being a headless CMS as well this seemed like a good fit. Especially since Ghost has a GraphQL API which is what Gatsby wants.

What I liked about my old setup with Hugo

  • It has a single executable with no dependencies. Run hugoand off you go
  • Very performant. Building my site took about 250ms on my Macbook
  • Many themes to choose from.
  • Easy to get going on a new post. Create a new markdown file and start banging on the keyboard.

What I liked a bit less

  • No asset management. If you want to use Webpack or babel for example you are on your own.
  • Images would either have to have been included in the repository or uploaded separately. This was a bit of a pain to deal with since I did not want all my images to clog up the repository.
  • Hugos templating system is not my favorite, especially when I wanted to override templates from the theme.

Comparison between old and new

FormatMarkdown filesGhost + GraphQL
WritingText EditorGhost
ImagesLocal filesGhost + Amazon S3
CSSMinimo themeTailwind
HostingAmazon S3/CloudfrontCloudflare Workers
DeploymentManualCircle CI

Gatsby + Ghost

I used gatsby-starter-ghost to get started and stripped out all the stuff I didn't need such as tag and author pages. This process took a few days or so until I was left with a clean setup. The starter also has sitemaps and RSS feed support as well as meta tags for Facebook, Twitter and more. All in all this process was pain free and I highly recommend this starter if you want to use Ghost with Gatsby.

Hosting the Ghost CMS

Since I use Ghost as a headless CMS I wanted to host it myself. This is done through one of my servers running Dokku following this guide. It was pretty easy to do until I wanted to upgrade to Ghost 3.0. This was a bit of pain to do through the use of Docker containers but I got there in the end. The database is running PostgreSQL and it is all backed up to Amazon S3 in case something goes wrong. The blog will obviously not go down if the Ghost CMS does since the CMS is only used during build time.


With the old setup I used my regular editor to write: VSCode. This worked just fine with a markdown plugin. However, I wanted to be able to write on multiple computers. I also wanted to avoid without having to install markdown editors on my Windows machine or use some kind of online markdown editor. The Ghost editor is so much nicer to use. It is available anywhere and a joy to use. The WYSIWYG tools are great, you can easily embed tweets, Youtube videos, and more. If you need to you can just use markdown to make some advanced formatting like tables. This is probably the best thing about my new setup.

While I think that markdown is a good way I still find it a bit easier to do it in the Ghost editor. Not to mention that I need my writing to be as simple as possible to remove any friction from it. I tend to procrastinate otherwise.

Image uploads

Handling images was a bit annoying in the old setup. I didn't want to have large images cluttering up the repo so I added them to a symlinked directory which was not on Git. This was ok but not great. The new setup uses the Ghost image uploader and the images are saved to Amazon S3. I also use Thumbor to manage image sizing and quality on the fly. I cannot recommend Thumbor enough for this purpose! Finally, the images are served through the Cloudflare CDN which makes everything super fast.


The old blog was style using the Hugo theme with very little custom styling. This looked ok but I wanted something that was unique to my site. Since my latest CSS darling is TailwindCSS I decided to go with that. I made something that looked pretty good in a couple of hours. You should give Tailwind a try if you haven't already!


Tailwind includes a LOT of CSS classes by default so you really need to use PurgeCSS. It will check your generated code at build time and then remove any CSS rules which are not being used. Gatsby has a plugin for this and that has support for Tailwind as well. After PurgeCSS has run the total CSS size is about 15kb. Excellent!

React for everything

Since Gatsby is based on React then essentially everything you do there will be React components. Your post templates will be React with Graphql queries to grab the data from Ghost and so on. This worked out a lot better than I expected, probably because you can find anything you need for React/Javascript. You don't have to worry about including scripts in your header or footer. Just use Javascript imports to include what you need and use it. The Gatsby code was surprisingly readable, even for someone like me who is not a React developer primarily.

Metadata and SEO

The Ghost starter has a complete solution to generate metadata and proper SEO stuff as well. You really don't have to do much to get this working. Since I did not want this data to come from Ghost I changed a few things however. Not a big deal.


You can add pages as well such as a projects page. I wrote a simple component for this that generates a link in the header menu for each page. It looks like this:

  const data = useStaticQuery(graphql`
        sort: { order: ASC, fields: published_at }
        filter: { tags: { elemMatch: { name: { eq: "#mskog" } } } }
      ) {
        edges {
          node {

  const path = globalHistory.location.pathname.replace(/.+\/$/, "");

  const pages ={ node: { slug, title } }) => (
    <MenuItem key={slug} href={`/${slug}`} name={title} activePath={path} />

I can now simply add a new page in the Ghost admin section. It will then be automatically added to the site and a link will appear in the menu.

Post pipelines

Ghost will return your post as HTML if you want complete with header tags, paragraph tags and such. In order to use Tailwind properly you need to add classes to these tags to make it look good. To do this I used cheerio which is like a lighter version of JQuery. Then I simply added a pipeline that all the body text passed through. It looks something like this for the header tags:

function headerTags(contents) {
  contents(":header").addClass("pt-3 leading-none font-sans");
  contents("h2").addClass("text-3xl pt-4");

Since the application is statically generated then this won't be a problem for performance. It is a bit hacky of course but it works reasonably well.

Hosting on Cloudflare Workers

The old blog was hosted on Amazon S3 and served through their Cloudfront CDN. This worked just fine and I had no real reason to change. However, I'd been looking for an opportunity to try out Cloudflare Workers Sites for a while now and this seemed like a good fit. Cloudflare Workers are essentially Lambda@Edge with faster startup time. They run WebAssembly and are thus very performant. With the Sites version you can deploy any static sites as well as SPAs. I use the Wrangler tool to deploy my blog. It really makes deployment a breeze compared to S3/Cloudfront. You don't need to fiddle with buckets and IAM roles and such. Just fill in some config data and the site is up.

For a very small site you can probably save some money by going with the Amazon route instead of Cloudflare though, since Cloudflare Workers has a minimum cost of $5 a month. This includes 10 million requests a month however and for this blog this is plenty. Traffic is included as well so don't worry about serving large numbers of images and such.

Performance wise it is ever so slightly slower than a cached Cloudfront response. ~15ms vs ~25ms but that is probably just because the Amazon data center is closer to me. That pesky speed of light has fooled me again!


The old page had manual deployment. I ran hugo in the console and then used s3_website to upload the build to Amazon S3 and invalidate the Cloudfront cache. The new setup has automatic deployment through Circle CI. Whenever I push to Github it will be picked up by Circle CI. It builds the project and then uses wrangler to deploy it to Cloudflare. This process takes about 2 minutes in total. Circle CI is overall an excellent solution for CI and we used it at work as well. It is free for open source, and they have a generous free tier for personal use as well.

Builds are also triggered when I publish something in Ghost or update a published article. Circle CI does this by using a web hook which triggers the build. This works really well and is easily done in the Ghost admin section.

You can also publish posts later in Ghost. This will trigger a build as well and the post will appear when you want it.

One Ghost, many blogs

An unexpected benefit of using Ghost as my CMS is that I can use it for multiple blogs if I want to. To do this I added a tag to every post and page. This tag is then used in every query in Gatsby to select only that content. I can now easily add another blog or site and use data from another tag. At least in theory!

What I like most about the new setup

  • The writing experience is superior. I can open my CMS on any device I own and just type away. Images are automatically uploaded to the cloud and embeds such as tweets and Youtube videos look great.
  • I now have a proper asset pipeline and I have access to any NPM package I want. I'm planning on building a simple "related posts" section and now I can just add it with ease.
  • I have my own themes that I designed myself and React+Tailwind is a joy to work with when I want to make changes.

What I don't like about the new setup

  • The writing experience is not WYSIWYG since it will not look the same once the post has been added to Gatsby. I sometimes still have to preview the site locally in Gatsby before posting it.
  • The GraphQL queries for all of this are a bit complicated since they are based on the Ghost API. Not a big problem but there is quite a lot of it.
  • I can make changes to my blog with ease now but what about six months from now? Will I have forgotten how to do it properly if I no longer use Gatsby during my day job?
  • Building the site is slow. Hugo did it in milliseconds. It's minor annoyance but still.


Overall this has been a great experience and I definitely like the new setup quite a bit more. I'm not sure if this will make me write more or better, but we'll see I guess. Gatsby is neat to work with and it will probably be my goto for any static site moving forward. It's build time performance trade offs are acceptable to me and having access to the entire NPM ecosystem is a massive advantage.

I do think that this blog is over-engineered though. Do I really have to use all this stuff just to get good looking text on a screen? I have a self-hosted CMS on a rented VPS. This is then supplying a GraphQL API to a hosted CI provider which runs a special script. The script builds a React based site to output some raw HTML. This is then served through a WASM-capable edge CDN network through the use of a service worker. To get some text on the screen. Is this really necessary? Couldn't I just have used Ghost as-is instead for example? Am I doing all of this just to avoid writing? Is this just procrastination? This is getting too real and I'm going to stop writing this now ok bye.

Get awesome stuff in your Inbox

I care about the protection of your data. Read my  Privacy Policy.