Why I Moved from Notion to Ghost for My Headless CMS
After building content on Markdown and Notion for my blog posts, I think I've finally found a CMS I can see myself sticking with for a while.
I've been dogfooding JamComments as I iterate on it, and as a part of that effort, I've committed to blogging a little more regularly. It works out because I like doing both of those things.
Along the way, I also decided to move away from using flat Markdown files for blog posts. I wanted something a little friendlier for "on-the-go" writing, and it was becoming a little too cumbersome to manage the publishing process via Git repository (there are solutions to this, but I'm not totally sold on any of them).
I like the authoring experience of Notion, and was intrigued by its relatively new API, so I took the plunge and decided to upgrade this statically rendered site with Notion as its CMS.
That transition was fine until until things started to hurt. Quickly enough, I began to realize Notion might not be the best platform on which to further scale this sort of content. In the end, I chose to make another switch – this time, to the open source version of Ghost. I'll break it all down a bit in the form of several numbered lists.
Issues w/ Notion
Notion's writing experience is very good, but in terms of using it as a headless CMS, there were a few key things I didn't want to tolerate anymore.
#1. Build times were slow. Last I checked, Notion's API doesn't support fetching all of a page's content at once. Instead, you're required to fetch a page's individual blocks and stitch the content together. That makes for a lot of API requests and a lengthy build, compromising not only the development experience, but also the update process. Making the slightest tweak to a single post meant waiting for another wrinkle on my face to surface and a gray hair to sprout.
#2. API limits were frustrating. The fact that API limits exist isn't a problem, but it sure becomes a bear when you need to spend a lot of requests building a single page (see the previous point). I'd commonly find myself blocked from making more requests (especially during development), and would need to wait a few minutes to get going again. Technically, there are ways around it (the response will let you know how long you need to wait via
Retry-After header), but doing so only further exacerbates the "slow build time" problem.
To avoid hitting limits in local development, I set up a local caching mechanism using lowdb. It did the job, but it always felt like more complexity than I should need to manage for a personal blog, and I occassionally became annoyed when I needed to remember to blow away that cache when I wanted to see freshest content.
#3. It's too easy to publish. There's no dedicated "save" or "publish" feature in Notion. When you make a change, it's "stuck," and totally eligible to be taken up with the next build of my site. I was constantly worried about accidentally tweaking the content, and later discovering I had butt-typed and sent an unwelcome change to my production site.
#4. Handling images was too complicated. I was initially excited about the fact that Notion's image URLs expire after some time and how I'd need to engineer some solution to make them more/less permanent. It was fun -- I used Cloudflare Workers and R2 to pull it off and even wrote a post about it. But in hindsight, it became another level of complexity I didn't really want to maintain or deal with later on. Plus, it contributed to the long build times.
#5. I want webhooks. At the time of writing, Notion doesn't offer any webhooks that fire when content is updated. I would have loved to use this for rebuilding my site whenever a post was added, updated, or deleted. Instead, I needed to rely on manually triggering a build myself as needed; or waiting for the cron job I have set up to rebuild after some time. I've seen people get creative to solve for this, but I wasn't willing to take on that challenge.
Searching for a Headless Option
I'm cheap and I really like my static site, so I knew moving from Notion would mean looking for good, affordable (free), headless options. WordPress was a top contender. I have a solid amount of experience with the platform and felt I could knock it out pretty quickly. But a few things deterred me from taking that path. Another list:
#1. Self-hosted WordPress doesn't support Markdown out of the box. I'd need to research, vet, and install a separate plugin. I just didn't wanna deal.
#2. There's no built-in membership/newsletter support. I'd need to yet again go through the plugin vetting process, or else stick with something like Mailchimp, which I didn't love.
#3. I'm a little burned out on WordPress. There's nothing very tangible to this one. I just wanted something that I felt was a little more... focused, cruft-less, and removed from that ecosystem.
Then Ghost Came Along
I've known about Ghost for several years and while I've always liked the vibe it emits, I had never tried it. But when I did, I was impressed. List time:
#1. The writing experience is slickkk. This is the thing that first got me hooked. The UI is modern, simple, clean, and intuitive. It supports Markdown-like syntax out of the box too, which made it real easy to get acclimated. I also got all of the blog goodies I previously took for granted (drafts, tags, featured images, etc.) – things I needed to cobble together with various fields in Notion. And there's also a dedicated "publish" button baked into the platform.
Keep in mind that the "slow" approach involved both pulling content from the API, as well as the fancy image handling I was doing with Cloudflare. Still, that's like nine times faster, and I get to shed a whole lot of complexity along the way. It was an easy sell.
#3. It comes with built-in newsletter/subscription support. To be honest, managing a newsletter and subscribers wasn’t even on my radar when I initially started looking at Ghost. But then I started to explore the feature and discover just how respected it is in the content world (many have migrated to it as an alternative to Substack). Mailchimp had always felt a little heavy-handed for me, and I liked the idea of managing both my content and subscribers in a single place. And once I toyed around with membership features of the REST API, it became pretty clear this was a serious alternative to what I had been using.
#4. It's got webhooks. And good ones! I can easily fine-tune when a new build is triggered, which is nice because it means it's not triggered whenever a meaningless event takes place (like when a draft is updated, for example). Here's a view into the hooks I have set up as of writing:
In all, it takes away all of the complexities I was getting sick of dealing with, and leaves me with features I actually care about.
Here's a First
Usually, soon after making a pretty large shift like this, I start feeling a hint of malaise over how it's all put together. It's the curse of any developer – soon enough, it becomes incessant enough to knock it all down and rebuild it with a different foundation.
But this time feels different. My current stack provides pretty much everything I've consistently wanted. Astro gives me a performant static site with a great developer experience (I've written about this). Plausible gives lightweight, privacy-minded analytics (this too). And now Ghost gives me a hassle-free CMS that I really enjoy using (refresh the page to see what I've written about this).
I think I'll be leaving things where they're at for a while.
Get irregular updates about new blog posts or projects.I won't send you spam. Unsubscribe whenever.
Alex MacArthur is a software developer working for Dave Ramsey in Nashville, TN. Soli Deo gloria.
- Wondering, did you look at Strapi? If so, what were your thoughts?