An attempt to build a Ghost blog for "free"

So I wanted to create a blog. Not just any blog – I want the blog to have at least a great writing experience for me. For the past few years, I've been writing on Medium, but since Medium changed their business model... writing and reading on their platform has become hell (Elementary OS's Cassidy summarized the issue well on their blog; Chaleb also wrote a similar analysis on this). WordPress is off the grid for me – it's costly, it's heavy, it's over-complicated, and it uses PHP.

Note: This is a documentation of how I figured out a "solution" to this problem that I've been thinking about. This is not a tutorial, nor will the solution work for most (see "The Catch"). This should not be considered a technical documentation. For detailed steps on how to set something like this up, please read the respective docs for each of the resources I used.

Step 0. The Idea

I want a platform that's comfortable for me to write on, and easy for me to customize at the same time. That's where a Ghost comes in. I have tried it in the past, and I really liked the way their content-management system is set up. But here's the catch: Ghost Pro costs a minimum of $29 per month to get started with, and even if you host it on a VPS, it will still cost a minimum of $3.5 per month on Vultr's lower-end machines. Of course, since Ghost is a Node app, I can technically host in on Heroku... but that instance gets hibernated very frequently and a Ghost instance takes quite some time to get back up.

Here's where the idea comes to mind. I don't need the content management system on at all times. I just need when I need to write. But I want my site to be available at all times. Since all I want from Ghost is the editing interface, why don't I pull a copy of the content and build it into a static site, and let the Heroku instance sleep as it wishes?

Apparently, there are already solutions for this. I can do this while writing a minimal amount of code. Neat.

Step 1. Ghost + Eleventy + Heroku

This is all made possible by a pre-made solution from the Ghost + Eleventy team. This is a very quick and easy solution – there are tons of ways that you can customize these resources to make the system work better with your own needs. But since most of the things I needed to build the bare minimum are already online, I saved myself from the extra effort (who am I to talk about not reinventing the wheels when I setup this entire stack when I could've used Jekyll or WordPress am I right?).

The backbone of the site is simply a full-fledged instance of Ghost. Well because Ghost is written in Node.js, I know it is possible to be deployed on Heroku, but it will require quite a bit of work to fit it ourselves. Luckily, there's already a pre-adapted version available on GitHub and Heroku Marketplace... and the amazing @SNathJr even upgraded the entire base to Ghost's latest release.

All that's left for me to do is to deploy the instance with a click of a button.

Elements Marketplace: Ghost on Heroku
Just a blogging platform

Step 2. ZEIT + Auto Rebuild

The "face" of the blog that we're building is generated by a static-site generator. Because my brain is currently too small to learn how to properly code in React, I chose Eleventy, a simple static-site generator, as well as Nunjucks, a templating framework by Mozilla. Ghost is amazing enough to provide us with a boilerplate to get started with, so I worked off that.

Deploying the front-end of the site is simple, but required some additional set-up for it to work flawlessly. I chose ZEIT Now as my static host simply because how amazing their platform is.

Create a simple now.json configuration file. Remember to replace the GHOST_API_URL with the URL/IP address of your Heroku Ghost instance, and set your SITE_URL to the URL where the actual static site will be deployed. Replace itsmingjie with your username/organization name on ZEIT.

{
    "name": "blog",
    "scope": "itsmingjie",
    "build": {
        "env": {
            "GHOST_API_URL": "https://api.blog.mingjie.info",
            "GHOST_CONTENT_API_KEY": "xxxxxxxxxxxxxxxxxxxxx",
            "SITE_URL": "https://blog.mingjie.info
        }
    },
    "routes": [
        {
            "src": "/(.*)",
            "status": 404,
            "dest": "/404.html"
        }
    ]
}

But before directly throwing the pre-built site up online, I need to set up Ghost's Content API. Follow the guidelines on Ghost's API docs, and grab the content key to replace the placeholder values in .env, .eleventy.js, and now.js. And voila! That's a working static site.

Here's the magic: since my site needs to be rebuilt every time I update my content, I can setup a webhook on your Heroku instance to notify ZEIT to rebuild on site update.  

My Webhook Setup

Step 3. Hook Buffering

Here's where I hit a weird spot. ZEIT rebuilding sure is nice, but I surely don't want my site to call ZEIT to rebuild the static pages every time I hit "save". To be honest, I do that rather frequently due to... insecurities... we'll talk about that later. So I thought, why not write a tiny service to buffer all those excess rebuild requests, so I can go easy on ZEIT's daily rebuild limits?

itsmingjie/ghost-buffer
Node app to buffer build hooks from headless Ghost instance. - itsmingjie/ghost-buffer

Step 4. Preventing (some) Unwanted Access

This part is easy. I just don't want people to be accidentally brought to my API page and wonder: "Hmm... What is this?" So I a tiny line to dumbly redirect them back to my static site, and injected them to the header of every page of the Heroku Ghost instance.

<script>
    if (document.location.host == "blog-api.mingjie.info") {
        document.documentElement.innerHTML = "<code style=\"color:lightgray\">You're attempting to visit the API endpoint of my blog. Please visit our blog at <a style=\"color:lightgray\" href=\"https://blog.mingjie.info\">blog.mingjie.info</a>.</code>";
        document.location.href = "https://blog.mingjie.info/" + document.location.pathname;
    }
</script>
Ugly JavaScript Injection

The Catch

Ok, so by now... I have a working solution for hosting a Ghost blog for free – at least temporarily. But you may soon notice that my blog is no longer served like a static site. Why did I move away from this solution?

The problem begins with the Heroku Ghost instance.

Ghost needs a few additional things than a Node environment to start running: an SQL database, and a media storage system (since Heroku's does not provide file storage). The Heroku package uses add-ons like JawsDB MySQL & Cloudinary to satisfy that requirement, but unfortunately, the free tier's allowance is way below my needs. The free SQL add-on only provides up to 5MB of storage. which is totally not enough for a content-heavy blog like this one.

So, what did I learn?

I learned that everything comes with a cost... so I've wasted a few hours figuring this out. But it's been a joyful journey. It's been a while since I've been this focused while coding.