In general, Ghost CMS has been a good tool for me. I've been pleased by the speed and reliability of the platform, with the few problems I have run into being fixed by the Ghost team pretty quickly. From the very beginning though I've struggled with the basic approach of the Ghost platform.
At its core, the Ghost CMS tool is a newsletter platform. This makes sense, it's how small content creators actually generate revenue. But I don't need any of that functionality, as I don't want to capture a bunch of users email addresses. I'm lucky enough to not need the $10 a month it costs to host this website on my own and I'd rather not have to think about who I would need to notify if my database got breached.
But it means that most of the themes for Ghost are completely cluttered with junk I don't need. I started working on my own CMS, but other than the more simplistic layout, I couldn't think of anything my CMS did that was better than Ghost or Wordpress. There was less code, but it was code I was going to have to maintain. After going through the source for a bunch of Ghost themes, I realized I could probably get where I wanted to go through the theme work alone.
I didn't find a ton of resources on how to actually crank out a theme, so I figured I would write up the base outline I sketched out as I worked.
Make your own Ghost theme
So Ghost uses the Handlebars library to make templates. Here's the basic layout:
/your-theme-name/
|
├── /assets/
| ├── /css/
| | └── screen.css
| ├── /js/
| | └── main.js
| └── /fonts/
| └── ...
|
├── /partials/
| ├── header.hbs
| ├── footer.hbs
| └── ...
|
├── default.hbs
├── index.hbs
├── post.hbs
├── page.hbs
├── tag.hbs
├── author.hbs
└── package.json
This is what they all do:
-
package.json(required): The theme's "ID card." This JSON file contains metadata like the theme's name, version, author, and crucial configuration settings such as the number of posts per page.
-
default.hbs(optional but probably required): The main base template. Think of it as the master "frame" for your site. It typically contains the , , tags, your site-wide header and footer, and the crucial {{ghost_head}} and {{ghost_foot}} helpers. All other templates are injected into the {{{body}}} tag of this file.
-
index.hbs(required): The main template for listing your posts. It's used for your homepage by default and will also be used for tag and author archives if tag.hbs and author.hbs don't exist. It uses the {{#foreach posts}} helper to loop through and display your articles.
-
post.hbs(required): The template for a single post. When a visitor clicks on a post title from your index.hbs page, Ghost renders the content using this file. It uses the {{#post}} block helper to access all the post's data (title, content, feature image, etc.).
-
/partials/ (directory): This folder holds reusable snippets of template code, known as partials. It's perfect for elements that appear on multiple pages, like your site header, footer, sidebar, or a newsletter sign-up form. You include them in other files using {{> filename}}
-
/assets/ (directory): This is where you store all your static assets. It's organized into sub-folders for your CSS stylesheets, JavaScript files, fonts, and images used in the theme's design. You link to these assets using the {{asset}} helper (e.g., {{asset "css/screen.css"}}).
-
page.hbs (Optional): A template specifically for static pages (like an "About" or "Contact" page). If this file doesn't exist, Ghost will use post.hbs to render static pages instead.
-
tag.hbs (Optional): A dedicated template for tag archive pages. When a user clicks on a tag, this template will be used to list all posts with that tag. If it's not present, Ghost falls back to index.hbs.
-
author.hbs (optional): A dedicated template for author archive pages. This lists all posts by a specific author. If it's not present, Ghost falls back to index.hbs
How It All Fits Together: The Template Hierarchy
Ghost uses a logical hierarchy to decide which template to render for a given URL. This allows you to create specific designs for different parts of your site while having sensible defaults.
- The Request: A visitor goes to a URL on your site (e.g., your homepage, a post, or a tag archive).
- Context is Key: Ghost determines the "context" of the URL. Is it the homepage? A single post? A list of posts by an author?
- Find the Template: Ghost looks for the most specific template file for that context.
- Visiting
/tag/travel/
? Ghost looks fortag.hbs
. If it doesn't find it, it usesindex.hbs
. - Visiting a static page like
/about/
? Ghost looks forpage.hbs
. If it's not there, it usespost.hbs
.
- Visiting
- Inject into the Frame: Once the correct template is found (e.g.,
post.hbs
), Ghost renders it and injects the resulting HTML into the{{{body}}}
helper inside yourdefault.hbs
file.
This system provides a clean separation of concerns, making your theme easy to manage and update. You can start with just the three required files (package.json
, index.hbs
, post.hbs
) and add more specific templates as your design requires them.
Source code for this theme
You are more than welcome to use this theme as a starting point. The only part that was complex was the "Share with Mastodon" button that you see, which frankly I'm still not thrilled with. I wish there was a less annoying way to do it than prompting the user for their server, but I can't think of anything.

Checking your theme
So Ghost actually has an amazing checking tool for seeing if your theme will work available here: https://gscan.ghost.org/. It tells you all the problems and missing pieces from your theme and really helped me iterate quickly on the design. Just zip up the theme, upload it and you'll get back a nicely formatted list of problems.
Anyway I found the process of writing my own theme to be surprisingly fun. Hopefully folks like how it looks, but if you hate it I'm still curious to hear why.