New look! I built a custom Ghost theme for this site from scratch
I run Ghost on my own server, got tired of the stock themes, and ended up building the whole thing myself — theme, publishing script, and all.
I self-host Ghost for this blog, and for a while I just used one of the themes it ships with. It looked fine. It also looked like every other Ghost blog, and it wanted me to write posts through a web editor every time. So I built my own theme, and a little script to go with it, using Claude Code to build it. This post is about that.
Why bother
Ghost's built-in themes are aimed at one kind of blog — a single topic, posted consistently. Mine isn't that. I write about golf, tech, and whatever else, and I wanted each of those to feel like its own little section without me building four separate sites. So the theme I made has four sections, each with its own colour, and a home page that just shows the latest posts without trying to cram every section onto it at once.
How the theme works
It's simpler than it looks. Every post gets a tag, the first tag decides its section, and one CSS variable colours the whole page to match:
[data-section="golf"] { --accent: var(--golf); }Everything else in the theme — headings, cards, links — just refers to that variable and doesn't care which section it's in. Add a new section later and it's one line of CSS and a tag, nothing to rebuild.
Writing without the editor
Since I run Ghost myself, it also has an Admin API, so I didn't have to stop at the theme. I wrote a small script that takes a Markdown file and publishes it — no browser, no editor, just a file and one command:
python3 tools/ghost_publish.py posts/my-post.mdIt always lands as a draft first. Publishing is a separate, deliberate step, on purpose.
What it doesn't do
Short list, because I kept it small on purpose:
- No tables, footnotes, or checklists — the Markdown converter only handles what I actually use.
- No image resizing or bulk photo uploads — one file at a time.
- No real scheduling, no preview before it's already a draft on the site.
- No comments, memberships, or extra authors — it's just my blog.
- No tests. I read the diff before I run it.
Why leave it that bare
Every one of those was a place where the "real" version would've been more work than the problem deserved. This was never meant to be a product — it's a self-hosted blog with one reader in mind while I'm building it: me. The goal was just making the gap between writing something and it being on the site small enough that I'd actually post.