Haunted No More

Goodbye Ghost!

Haunted No More

The Backstory

Ever since John O’Nolan launched his Kickstarter campaign for Ghost I’ve been following the project closely. Not only because for what it was worth the idea got backed by a ridiculous amount of users (and hence money) within a super short period of time, but also because I was excited to see a clean and minimal take on the whole blogging topic. Back at that time the blog ecosystem was a huge mess of largely PHP-based software, with some less awful exceptions coming out of the Ruby camp. John’s promises back in 2013 were very luring: An extensible and theme-able Node.js/Express platform where Markdown formatting is a first-class citizen, with a dashboard that implements every metric that might be relevant to you and all that in a responsive, mobile-friendly package.

Fast-forward to today. The platform has had close to one decade to grow and deliver on the promises that were made initially. And in some areas it surely did. For example, there are plenty of themes – mostly paid ones – available these days. Unfortunately, theme development is still cumbersome and lacks of many important features. Even the tooling required for development turns out to be rather immature.

Side-note: While digging into the linked issue and similar ones I’ve noticed that Ghost’s GitHub account isn’t well maintained, with issues piling up and lacking response from the maintainers in parts. So, if you’re one of those maintainers or maybe even part of the Ghost leadership and you see a user that investigates and fixes an issue, on your project, in his free time and then even walks the extra mile to do the job you should be doing and update all related issues, then maybe don’t just pick on the single most irrelevant line you can find and be a total douchebag about it without providing any value on the actual topic whatsoever. Just a hint.

Technical Debt

Apart from the very weird tone the Ghost leadership seems to have towards their own community, the software itself speaks a similarly harsh language from a technical perspective: The past seven years flew by and left behind Ghost, which is stuck in largely crude JavaScript, not profiting from advancements like Flow or Typescript or even just basic ES6 syntax-sugar like classes and module import/export. And it’s not only the code that hasn’t aged well: Most Ghost integrations are pretty much stuck in the past as well. Take the S3 integrations for example. Both projects linked on the official Ghost integrations site haven’t been updated in over a year. It’s unsure if those even work with the latest Ghost 3.x versions.

All in all the major selling points for Ghost either still don’t exist or took an unexpected turn at some point. For example, there still is no dashboard that aggregates all metrics in one place. That’s probably one of the the most prominent features that was expected by a large amount of Ghost’s user base but got scrapped sometime along the way. “Fortunately, though, you can still make the dashboard yourself really easily using Geckoboard, which is a fully thought-out, feature-complete product” … that is not open-source, which was one of the selling points during the Ghost Kickstarter campaign, and also happens to costs users at least $31 per month (on a yearly plan) in addition to the costs of either hosting their own Ghost blog or paying a whopping $29 per month for a full year on Ghost’s Basic SaaS plan, which is limited to 100.000 views per month. Business numbers aside: How is it, that Ghost seemingly can measure views (and probably other things) internally but can’t throw these numbers on a dashboard? Just a thought.

In conclusion, Ghost has …

  • no integrations
  • no dashboard
  • and a lot more no-nos on the technical side

Just like many others I tried to work around all those shortcomings for the past few years in order to continue using it to power this site amongst others.

The Impact of Running Ghost

Hosting a Ghost installation might seem easy at first. Ghost provides you with an instant-noodles-version of their platform that one can get up and running in no time. The Docker image starts Ghost by default with a SQLite database. That’s a single-file-datastore that lives inside the container and makes Ghost super cheap to run, as long as you have access to a Docker host. Many cloud providers like Digital Ocean offer one-click installations of Ghost that run exactly like that.

“The bitterness of poor quality remains long after the sweetness of low price is forgotten.” ― Benjamin Franklin

Obviously hosting Ghost this way leads to tricky issues further down the road. Yes, it’s super cheap at first, but what if your instance/droplet crashes? What if traffic on your blog spikes and you need to scale your system for it to sustain? There are plenty more questions where those came from. Long story short, if you’re serious about software, you’re not going to host your site that way. Instead, you will most likely end up connecting a dedicated database to it, what will add another few dollars to your monthly hosting bill. But then you’re good to go, right?

Well, not quite. Ghost only stores the text/markdown/HTML content inside the database. Files (like uploaded images) are still being stored in a folder inside the container. Assuming you would scale from one Ghost instance to two or more, you would end up with files that will only be available on the instance you were connected to while performing the file upload through the admin interface. This means that every user that browses your site will only get access to the files that are available on the instance that he will be assigned to by the load-balancer in front.

“But you could install a storage adapter that uploads all files to S3 buck… oh, wait."exactly that. Ghost, as of right now, is pretty much build in a way in which it works for anyone who’s not dealing (or planning to deal) with huge amounts of traffic and therefore won’t need to scale to > 1. But as soon as you should be reaching peak capacity, scaling horizontally isn’t effortlessly possible, unless you’re unconcerned throwing a service like Cloudflare in front, which takes care of caching your site so it won’t get run-down by peaks. And frankly speaking, it would be too much of a hassle to even try to deal with it yourself, since the value Ghost provides in total is so little, that it makes absolutely no sense to go the extra hundred miles to have a scalable Ghost installation that performs well and delivers content quickly. That’s pretty much when most people (and companies) need to decide whether they want/have to continue using Ghost – for whatever absurd reasons they might come up with – and therefore subscribe to Ghost’s software-as-a-service, where a handful of engineers I really don’t envy took care of scalability, or whether they move on to something else. Which is what I did.

Meet Hugo

Hugo is an open source static website generator written in Go that runs on all modern operating systems as well as on even more antiquated ones like Solaris, Plan 9 and Microsoft Windows.

Initially I fancied Zola, mainly because it’s written in Rust – the faster, more beautiful but less productive Go™ – but unfortunately it couldn’t keep up with the sheer size of the community around Hugo.

With Hugo, I could easily migrate my existing Ghost theme, since it features a templating syntax similar to Handlebars. Also migrating my markdown content was easy… at least that’s what I thought initially. Unfortunately Ghost uses mobiledoc internally, which happens to be a cumbersome solution for solving a mundane problem. However, I managed to write a couple of Deno scripts and use a few toolsnot jq – that allowed me to migrate my content (including all images) to Hugo’s structure and format.

The Nice Parts

A statically generated site allows one to do all sorts of nice things that can usually be quite tedious with a dynamically generated site like a Ghost blog. For example, since no database connection is required, I was able to build a lightweight container that could easily be scaled horizontally on the Kubernetes cluster that I’m using to host this site (amongst other things). In addition to that, I was able to adjust the deployment process (which runs a clean GitHub Workflow/Actions CI) in a way in which only the actual HTML is being injected into the container, while all assets (mainly CSS and images) are being deployed directly onto a CDN. In theory one could simply deploy the whole site on a CDN and wouldn’t require to run any containers whatsoever. However, since I’m also making my site available on DAT (not anymore, since it’s in a weird state of being partially dead and partially rebranded) and on IPFS (via ipfs://ipns/xn–gckvb8fzb.com) I decided to not have it solely on a CDN.
Having a Ghost blog available on DAT or IPFS is a very cumbersome process that requires some hacking, but with a static website it’s pretty straightforward.

With the migration to a statically generated site I also finally took the time to build my very own theme, which was already praised as being “brutal”. I didn’t use any CSS framework whatsoever, although I wish that ZEIT’s new.css would have been available at the time I rebuilt the site. And yes, I still refuse to call them this weird pharmaceutical product name they were given in exchange for $21M.

Another thing I didn’t use was JavaScript. Okay, that’s a lie. There are 16 lines of pure JavaScript code that I use for collapsing/expanding the site menu on mobile. And there are things like the privacy-focused analytics tracker script that I’m using to track how many people read the things I write or embeds of tweets, toots, Mapbox maps and Apple Music songs – all of which aren’t critical site functions and wouldn’t be missed if you’d be having JavaScript disabled in your browser. But apart from those, there’s no line of JavaScript code running on this site that’s crucial to its functionality. So, feel free to turn off JavaScript if you’re browsing this page on a large-enough display.

In addition to the lack of a CSS heavyweight (like Bootstrap) and JavaScript, this site doesn’t show any cookie information popups. Why? Well, simply because this site does not use cookies.


Moving away from Ghost not only allowed me to run this site with fewer resources (in terms of processing power and memory on the hosting-part), less complexity and a better integrated deployment process, but it also simplified the content creation as a whole. Since I cannot rely on great internet connectivity, I never used Ghost’s admin panel to write new posts. Instead I use NeoVim or iA Writer to prepare the posts and then manually copy & paste everything into Ghost. Especially file uploads were pretty tedious to handle, since Ghost would not allow me to pre-define how files will be named and where they will be located once they’re uploaded. Long story short, Hugo allows me to prepare all new content locally, flag it for publishing as soon as it’s ready and simply commit it to a git branch to have it deployed within a couple of minutes to HTTP (+ CDN) and IPFS. Assets (like CSS and images) are being compressed during deployment so that content delivery is blazing fast for the clients (that’s you). In fact, according to GTmetrix the site loads in usually around 1 second (sometimes even less than that), with a total page size of ~60kb and only 7 requests, out of which most of them are for fonts. These numbers are pretty cool, considering that my site is only twice as slow as and only three times the size of a regular motherf*cking website. That’s something that would have been pretty hard to achieve using Ghost, at least without putting any concerning service like Cloudflare in between to do the caching and delivery.

(PS: Google.com loads at 1.0 seconds flat, with a total page size of 415kb and 22 requests firing)