Custom Ghost Theme Development (Handlebars)
Ghost themes use Handlebars — logically simplified templating language. Unlike WordPress PHP templates, Handlebars has no arbitrary code — only Ghost helpers and built-in expressions. This limitation ensures security but requires understanding helper system.
Theme Structure and Required Files
my-theme/
├── package.json # required: name, version, engines.ghost
├── index.hbs # homepage / posts list
├── post.hbs # post template
├── page.hbs # static pages
├── error.hbs # error pages (404, 500)
├── tag.hbs # tag archive (optional, fallback to index)
├── author.hbs # author page (optional)
├── partials/ # reusable parts
│ ├── header.hbs
│ ├── footer.hbs
│ └── post-card.hbs
└── assets/
├── css/screen.css # main CSS
└── js/main.js
// package.json
{
"name": "my-theme",
"description": "Custom Ghost theme",
"version": "1.0.0",
"engines": { "ghost": ">=5.0.0", "ghost-api": "v5" },
"license": "MIT",
"config": {
"posts_per_page": 12,
"image_sizes": {
"xs": { "width": 300 },
"s": { "width": 600 },
"m": { "width": 1200 },
"l": { "width": 2000 }
},
"card_assets": true,
"custom": {
"header_style": {
"type": "select",
"options": ["Center", "Left", "Right"],
"default": "Center"
},
"show_reading_time": {
"type": "boolean",
"default": true
}
}
}
}
Core Helpers and Context
{{! index.hbs — posts list}}
{{#foreach posts}}
<article class="post-card {{post_class}}">
{{#if feature_image}}
<figure>
<img srcset="{{img_url feature_image size="s"}} 300w,
{{img_url feature_image size="m"}} 600w,
{{img_url feature_image size="l"}} 1000w"
sizes="(max-width: 768px) 100vw, 50vw"
src="{{img_url feature_image size="m"}}"
alt="{{title}}">
</figure>
{{/if}}
<div class="post-card-content">
<header>
{{#primary_tag}}
<a href="{{url}}" class="post-tag">{{name}}</a>
{{/primary_tag}}
<h2><a href="{{url}}">{{title}}</a></h2>
</header>
{{#if excerpt}}
<p>{{excerpt words="30"}}</p>
{{/if}}
<footer>
{{#primary_author}}
<img src="{{img_url profile_image size="xs"}}" alt="{{name}}">
<a href="{{url}}">{{name}}</a>
{{/primary_author}}
<time datetime="{{date format="YYYY-MM-DD"}}">
{{date format="DD MMM YYYY"}}
</time>
{{#if @custom.show_reading_time}}
<span>{{reading_time}}</span>
{{/if}}
</footer>
</div>
</article>
{{/foreach}}
{{pagination}}
Post Page
{{! post.hbs}}
{{#post}}
<article class="{{post_class}}">
<header>
{{#unless primary_tag.name}}{{else}}
<a href="{{primary_tag.url}}" class="tag">{{primary_tag.name}}</a>
{{/unless}}
<h1>{{title}}</h1>
{{#if custom_excerpt}}
<p class="excerpt">{{custom_excerpt}}</p>
{{/if}}
<div class="meta">
{{#foreach authors}}
<a href="{{url}}">{{name}}</a>{{#unless @last}}, {{/unless}}
{{/foreach}}
·
<time>{{date published_at format="DD MMMM YYYY"}}</time>
· {{reading_time}}
</div>
</header>
{{#if feature_image}}
<figure class="hero-image">
<img src="{{img_url feature_image size="l"}}"
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}">
{{#if feature_image_caption}}
<figcaption>{{feature_image_caption}}</figcaption>
{{/if}}
</figure>
{{/if}}
<div class="post-content gh-content">
{{content}}
</div>
{{! Related posts}}
{{#get "posts" limit="3" filter="tag:[{{primary_tag.slug}}]+id:-{{id}}"}}
<section class="related-posts">
<h3>Related articles</h3>
{{#foreach posts}}
{{> post-card}}
{{/foreach}}
</section>
{{/get}}
</article>
{{/post}}
Dynamic Data with {{#get}} Helper
{{! Latest posts by tag on homepage}}
{{#get "posts" limit="5" filter="tag:javascript" order="published_at desc"}}
{{#foreach posts}}
<a href="{{url}}">{{title}}</a>
{{/foreach}}
{{/get}}
{{! Subscriber count (public stats)}}
{{#get "tiers" limit="all"}}
{{#foreach tiers}}
<div>{{name}}: available for {{monthly_price}}/month</div>
{{/foreach}}
{{/get}}
Build and Validation
# Validate theme (official Ghost tool)
npm install -g gscan
gscan /path/to/my-theme
# Or via API (CI/CD)
gscan /path/to/my-theme --json
# Upload via API
curl -X POST https://myblog.com/ghost/api/admin/themes/upload/ \
-H "Authorization: Ghost $ADMIN_API_KEY" \
-F "[email protected]"
Timeline
| Theme | Time |
|---|---|
| Minimal working theme | 2–3 days |
| Full-featured theme (index, post, tag, author) | 5–8 days |
| Theme with Members/paywall and custom settings | 8–12 days |
| Theme with Newsletter templates | +1–2 days |







