cd /cohost-blogger/

Escaping CMS hell into CMS heaven

· by jill

How I wrote cohost-blogger, my own custom blogging software solution built entirely on Cohost, and why GhostCMS fucking sucks

For most of's lifetime, I've used Ghost as my CMS of choice. The main reason is because it's fully open source and self-hostable - which was a perfect fit for a VPS I had gotten my hands on not too long before then for hosting

It was fairly simple to set up - I mean, you can't really overcomplicate just a plain CMS (foreshadowing) - so, I, blissfully unaware of the consequences of my actions, decided to for the foreseeable future, use it for my general thought-dumping purposes.

It was quite fun learning how to write stuff! Looking back at the older articles now, they're.. not too amazing, but I had fun, and still am. Being able to dump my thoughts about projects or ideas onto a digital sheet of paper and then be able to show it off felt quite nice.

Fast forward to now - and Ghost goes down practically every month, I've had to reinstall it probably 10 times, and it actively compromised the security of my system before I replaced it. What happened?

What happened

Ghost Kinda Fucking Sucks

Let me walk you through how this went, dear reader.

At first, the set up was really easy. It let you pick between PostgreSQL and MySQL for a database engine, and I already had Postgres set up, so I figured I'd use that. I started writing posts, and it was generally a nice experience. So, a few months later, I did a routine update check, and...

Okay, well - that's not ideal, but not the end of the world. I could export the entire site, set it up again, but with MariaDB (a fork of MySQL which I already had set up for a separate thing) and it'd be fine, right?

I did that, and it worked, but then one day, Ghost updated to a version of MySQL not supported by MariaDB. And it all blew up once more. Hm.

That wasn't ideal either, but - I guess I could move everything over to MySQL then, right? Wrong! The databases weren't cross-compatible or convertible, and I couldn't run both at the same time. This was becoming extremely inconvenient now, but I had already stuck with Ghost for so long that I simply couldn't Just Switch to something else.

After stressing out at 5am, scrambling to fix this, wondering if I could ever even run Ghost anymore, I threw together a temporary solution - using SQLite as the database instead. Keep in mind Ghost doesn't let you use SQLite unless you're in a development environment - so I had to spend quite a bit setting it up again, now trying to fool it into thinking it's in a development environment.

Cool. Now it worked again. But it was a very very fragile system - SQLite is very, very much not meant for production. SQLite works fully in a single file, rather than a conventional daemon like most databases, which means it's at the mercy of your disk's raw speed for performance. I had a low enough visit count where I could deal with that, but it wasn't something that could last for long.

After a while, though, I migrated to a new server - dark-firepit - which was on NixOS, a Linux distribution that behaved structurally VERY different to how Ghost expected it to. You can't just install ghost-cli, run the installer and be done with it - if you want a package, especially a service, to run, you will want it to be built and managed with Nix.

I'll avoid dwelling on this for too long - but just know that system administration with NixOS is probably one of the most purest forms of joy you can feel in this world.

I put off tackling this for many months, until I had to, since the old server was being shut down in favor of the new one. I found an implementation of it in Nix someone else did, and figured it would work well enough. Nope!

Turns out, this packaging of Ghost relies on the NixOS sandboxing being disabled, which was a massive part of the system's security. During the build step, it installs NPM packages, which simply isn't possible without network access, which is cut off at that point. So now I had to temporarily disable it while I look for a fix or an alternative to Ghost. Great.

It is also worth noting this was extremely painful to figure out on my own.

At this point, I was getting really, really annoyed at Ghost and its continuous efforts to make my time dealing with it as painful as physically possible. It did not want to be used the way it was not intended to, and it was getting quite frustrating. Issue was, I had no alternative to run to that I could trust wouldn't do the same thing.

It was near this time when I tried developing my own CMS - and failed - cementing the fact that getting out of this situation will not be easy.

You Know What Doesn't Fucking Suck?

Cohost is a micro-blogging social media, Tumblr-like, whatever you want to call it, site thing. You can post on it. That's the important part here.

You, and your fellow chosters, get to make posts that others can repost (rechost) or like (chlike???). You can additionally comment on posts, but you can't like them or repost them, only reply.

Now, what brings this all together is that Cohost is a little special. Cohost lets you embed arbitrary HTML in your posts, so long as there isn't any JS. This has led to a pretty special genre of posting, referred to as CSS crimes.

I was immediately attracted to it - the demoscene-ish nature of making cool-looking things (or even functional things!) in just inline CSS styles was extremely alluring. Thanks to an invite (this was a while ago now, when the sign-up queue was extremely long), I managed to get in, and chosted all over those guys.

I spent an incredible amount of time coming up with ideas for crimes and realizing them, all of which you can view under the #oatposting tag. I got quite acquainted with the site and its wonderful little post creator, thanks to prechoster.

Through doing this, I slowly learnt the ins and outs of the site's culture - realizing how incredibly well-crafted the site is. Its lack of numbers discourages the typical negativity a social media comes with, and the userbase and developers are made up completely of gay furries. It felt like I had done it - I had found a social media that really works for me, unlike something like Tumblr or Mastodon, where my previous ventures have been.

Turns out, I actually really like this site.

I spent more time on the site dumping my thoughts as I otherwise would on my blog, and this led me to make a little revelation...

What If I Simply Used Cohost Instead?

Even with just plain markdown, I could express myself quite well back in my days on Mastodon, but now that I had access to HTML, I could do whatever extra things I wanted in my posts, making them feel truly mine.

And - I get to have the posts hosted for free, without the pain of writing an actual, full CMS. It was perfect - aside from one little issue...

Ghost Still Fucking Sucks (And Haunts Me (Haha) To This Day)

This was around the time I got properly fed up with Ghost, and while I wanted to just abandon it in favor of Cohost, I can't exactly do that, as I have to preserve my previous posts and have a convenient way to display them without having to go on Cohost to see them.

So then one day, I came up with the genius idea.


What if I made a blogging site that used Cohost posts as the backend for its posts? Sending API requests to Cohost to retrieve posts and comments and displaying them nicely was all you really needed. It was too good to be true - and I spent a good few days asking myself if I was truly onto something, or if this was a stupid idea.

So, I got to work! (And you can see my process for this as it happened in the #cohost-blogger notes tag.)

The making of cohost-blogger

The start of this was a light bit of Cohost reverse-engineering - there is currently no public API, so I had to make do with the Firefox Network tab and a light amount of ingenuity.

At first, I settled for just fetching the HTML of the desired tag and parsing trpc-dehydrated-state. This worked, but very slowly. Then, with the help of @mintexists, I figured out how to do this with just a TRPC request:

const url = `${encodeURIComponent(JSON.stringify({
  projectHandle: 'oatmealine',
  tagSlug: 'oatposting'
const data = await (await fetch(url)).json();
return {

That was the easy part done - now I had the posts available to me.

My next step was figuring out how I could render them into HTML - the API gives me three options:

  1. plainTextBody, which contains just what was typed into the editor and nothing more,
  2. blocks, which has a list of Markdown blocks after an initial parsing step, and
  3. astMap, containing a JSON-ified HTML dump of the rendered post.

I stuck with astMap, since it was the easiest - and it worked! I could successfully render my own posts, even the more convoluted ones.

I began porting over the posts from my old blog to Cohost with the help of prechoster (big shoutout to prechoster) and it was going well, but then I realized a small issue - I would need to repost these, which could become spammy.

I figured I couldn't just dump them out - I had to provide a little disclaimer, saying that it's a reupload (and potentially of a very old post), but I had no clue how. So, I came up with the idea of making it a special class that's removed on the site, but kept on Cohost's side:

The issue is that Cohost strips all classes, and therefore I couldn't grab them from astMap. So, once again with the help of @mintexists, I used the Cohost source maps to borrow their Markdown rendering stack.

And, this worked! Really well. So well, that when I moved on to porting video and YouTube embeds, it was extremely simply to wrangle Cohost's Iframely integration to work for those.

With that done, I moved on to the URLs of the posts. You see, on my old blog, each post was assigned a slug by me - a simple, lowercase string - to identify it in the URL. If I were to move my old posts over, I couldn't just keep Cohost's /1234567-post-summary-here format.

So, I had to find a way to store metadata in the posts, for slugs and other miscellaneous information. My solution was to leave it all in a comment:

This, once again, worked incredibly well! In fact, I could grab this from Cohost's blocks, without spending time on parsing the Markdown.

Last, but not least, I had to figure out the rendering of the Markdown. Sure, I parsed it and had it neatly outputted into HTML, but it didn't look great.

It was at this point that @HeySora was selling me on Tailwind (and I was entirely sold) - a CSS framework. Specifically, she told me about the typography plugin that does exactly what I wanted - format article-style text from Markdown, a CMS or similar with nice defaults.

There was just one issue - using Tailwind in a project that hasn't used it from the start, as it is with all frameworks, is absolutely miserable. So, I did what anyone would do, and committed style thievery.

Sora told me I could export everything related to the prose class and be done with it:

module.exports = {
  // ...
  safelist: [/prose.+/]
  // ...

Doing that and taking the compiled CSS worked! It left me with a nice base to work with, setting sane defaults on general text rendering.

And, with that, I had done it! I had finally finished every part of a fully functional CMS!

Best of all - it was all with code I had written, letting me customize every part of it to my heart's content, tweaking certain things, adding nice little touches, and generally making it my own blog site than someone else's.

I had done it! I had found a way out of my miserable experience with Ghost. After porting all of the previous posts, it was in no way inferior than my previous solution. It was perfect!

And here we are now. cohost-blogger is now properly deployed on, and its source code is available on my Git page. It's not anywhere near being conventionally self-hostable, but it works for me, and that's what matters!

I hope you enjoyed reading about my time making Cohost do things it's not meant to, as I always do. :)


When I started my blog, Ghost started out on the short list of possibilities. For various reasons, a few weeks into the process, the short list was all static site generators (and I ended up going with Jekyll), because they all looked too fussy and too prone to unexpected failure like this.

I almost wish that I had done something more ambitious like this...


ooh, nice. i'd been thinking about doing something similar at some point but i wasn't sure how to solve the migrating old posts problem, and i had also been considering ghost. wild that it doesn't support postgresql


yoooooooooooooo this kicks ass

EDIT: i just saw this comment in context on your site and it looks like you're missing the egg avatar shape lmao


for chrome it looks like you need to prefix the 'mask' css stuff with '-webkit-'


this is super cool! I especially love the creative solutions for embedding blog-specific and even cohost-specific metadata in the post body


This is super cool and honestly kinda making me want to do something similar for my own site. Also now all your stuff is crossposted to cohost, fun bonus!


okay, yeah, I tried this to replace my old blog and cohost-blogger Just Worked. easy to install, easy to make changes, even easy to go back into my posts and retroactively add stuff to just have it show up. helps that I didn't have a lot of stuff to carry over from my old blog either. sick work, fantastic job.


hey! (@sirocyl speaking)

just wanted to let you know, we're using this for our blog over at :)

it's a really cool codebase, and we were able to get it up and running pretty effortlessly, which is a huge plus.

being that we're a Linux system unlike the others, we're probably the first OS with its own cohost blog lol


Only thing I'd like to ask is if there's a source license we can adopt the code under, so we can be comfortable sharing our changes too :D


i've been planning to put it under a license but i'm not exactly sure what just yet. gpl3 is my current pick; consider it that until i officialize it