Setting up a headless Ghost CMS with Next.js


I’ve been wanting to play with Next.js and Vercel for a while and my plan was to re-build the SPC website using a headless CMS.

For a while, the site has been running on Statamic v2, which has provided some great performance results which I was more than happy with and would be equally happy sticking with (or upgrading to v3). However, I get bored easily and I simply wanted to experiment with a JS frontend and a new CMS backend, and as I’ve been recently deep-diving with React, Next.js just made sense and has the added benefit of decent SEO features (and relieving some boredom).

I typically work with WordPress as my default CMS but I’m becoming increasingly frustrated with the block editor experience – through my own personal experience as well as the clients that I work with regularly. It’s very much a work in progress, I get that, but it just feels clunky to work with and WordPress as a whole is starting to head more towards a Squarespace or Wix type of setup – which neither myself nor the clients that I work with are interested in. So, whilst I still love WordPress and continue to work with it daily, I wanted a change for my own website.

Choosing a CMS

With the direction of WordPress in mind, I’ve been looking at alternatives such as Ghost, Sanity, and Contentful etc., which offer a nice admin experience with plenty of content options without going beyond this into a full site editor, and without having the need to build lots of constraints around the components so that the design doesn't get broken when they're used. Whilst both Sanity and Contentful both look like great options, I opted for Ghost for the following reasons:

  • It’s free as a self-hosted version – no usage limits etc
  • I can host all of the data
  • No need to set up lots of different content fields
  • There’s a one-click app available on Digital Ocean, making setup super easy
  • There’s a Next.js Ghost example to work from
  • It has a nice and clean admin interface for future client-use
  • I like the logo 🤷‍♂️

I’ll probably try out this process with Sanity and Contentful at some point to see how they compare.

Main aims

The main aims for this project were:

  • The ability to quickly create content with enough flexibility to add media and interesting layouts without breaking out of a design style
  • Green lights across all Lighthouse metrics without breaking the bank
  • Content must be available for search engines
  • Self-hosted data to keep costs under control


Using Ghost as a headless CMS ticks the box with regards to a simple and efficient admin experience, and it can be self-hosted to keep control over the data and costs. Pulling content from the Ghost API using a JS frontend will provide a good foundation for performance and allow for the content to be pulled into a frontend build to keep the design styles intact.

Setting up Ghost

Digital Ocean offer a one-click install of a Ghost instance within their Marketplace, so this installation was pretty straight forward using this guide – How to install Ghost on Digital Ocean.

I selected a $5 droplet because the Digital Ocean end of the bargain doesn’t have to be fast due to Next.js’ caching and static content functionality.

Once I’d ran through the setup, I logged into the Ghost admin and migrated my content from WordPress to Ghost using the Ghost WordPress plugin. I had some issues with exporting the media due to file sizes, but simply exporting the JSON file and manually uploading the WordPress images to the Ghost content folder solved this issue. As I was changing domains, I also ran some basic find and replaces in the JSON file using a text editor and checked that the image paths were correct.

Finally, I password protected the Ghost installation so that people wouldn’t be able to access the frontend.

Setting up Next.js

Whilst there are great docs out there which show how to couple up Ghost to Next.js, I wanted to use the Next.js Ghost CMS repo as it was pretty much as simple as cloning the repo and getting started with styling. They have plenty of these examples on the Next.js website.

In the end, I did end up refactoring the app quite a bit. I refactored the class components over to functional components to give easier access to hooks. For example, I wanted to use useRouter() to determine the current page, and functional components (as far as I know) make this easier.

The next job was to bring in Tailwind CSS and start putting together the design. In this case, I didn’t bother starting out with Figma. As I’m the only stakeholder on the project, it made sense to jump straight into the browser and design the site with code. This allowed me to test the performance metrics during the design phase to ensure that I was making design choices that aligned with my main aims.

Keeping the design simple and relying on clean negative space, coupled with some lovely fonts from Klim Type Foundry, made it pretty easy to get those green Lighthouse scores off the bat. The CSS file is just 2.5kb.

As an aside, I often get requests to work on a website’s performance to increase Lighthouse scores. It can be very difficult to achieve good scores once there are lots of plugins installed (in the case of WordPress, for example) and off-the-shelf themes are used. Performance should be considered as part of the design of a website or web application. Making smart design choices at the beginning absolutely affects performance and it should not be an after thought. Bringing in somebody once a website is already built is like trying to optimise the foundations of a house once everybody has been living there for a few years. Make performance an important part of your project right from the start and you will see huge improvements in the long term.

Back to the book. Finally, it was a matter of setting up the static page content. For this, I opted to just hard-code the content into Next.js components instead of adding them to Ghost – simply because they probably won’t change much so I don’t really need a CMS to manage them.

Deploying with Vercel

Getting set up with Vercel couldn’t be more efficient. Create a project, connect the GitHub repo, set up your API keys in the environment section, and hit deploy. Within a minute or so you get a working app on a staging URL 🚀 – hashtag shipped… or liftoff… or, launch 🤷‍♂️. To launch the site, I simply added my domain using their docs and it was live.

Subscribe to Steve Perry Creative

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.