The master Ghost tag/helper dictionary, theme architecture and starter boilerplates, and routes.yaml reference for building custom Ghost themes.
When designing a custom Ghost theme using Handlebars, each tag object exposes specific data properties you can render or evaluate:
| Attribute Key | Data Type | Description | Sample Handlebars Tag |
|---|---|---|---|
id |
String | The unique, incremental database identifier for the tag[cite: 1]. | {{id}} |
name |
String | The human-readable display name of the tag[cite: 1]. | {{name}} |
slug |
String | The URL-friendly, lower-case identifier used in routing[cite: 1]. | {{slug}} |
description |
String | The tag description text entered in the Ghost Admin panel[cite: 1]. | {{description}} |
feature_image |
URL String | The cover/feature image URL associated with the tag archive[cite: 1]. | {{img_url feature_image}} |
accent_color |
HEX Code | The primary accent color assigned to the tag in Admin settings[cite: 1]. | {{accent_color}} |
url |
URL String | The relative web address for the tag's dedicated archive page[cite: 1]. | {{url}} |
meta_title |
String | Custom meta title configured for SEO on the tag's page[cite: 1]. | {{meta_title}} |
meta_description |
String | Custom meta description configured for search engines[cite: 1]. | {{meta_description}} |
Used to categorize content for readers[cite: 1]. Public tags show up on post meta areas, build index archive pages automatically at /tag/slug/[cite: 1], and are fully exposed to the public Content API[cite: 1].
{{#foreach tags}}
<a href="{{url}}" style="color: {{accent_color}};">{{name}}</a>
{{/foreach}}
Internal tags start with a hash prefix (e.g., #newsletter, #hide)[cite: 1]. They are hidden from site readers and API outputs[cite: 1], serving as developer flags to switch layouts or alter logic[cite: 1].
{{#has tag="#newsletter"}}
{{> "components/newsletter-hero"}}
{{else}}
{{> "components/standard-hero"}}
{{/has}}
Standard required and optional directory blueprint including core theme templates, partials, assets, and custom page templates:
Complete starter blueprints for every file in the theme architecture, plus specialized custom page templates.
Core Theme Files
{
"name": "custom-ghost-theme",
"description": "High-performance custom Ghost CMS theme starter kit",
"version": "1.0.0",
"engines": {
"ghost": "^5.0.0"
},
"license": "MIT",
"config": {
"posts_per_page": 10,
"image_sizes": {
"xxs": { "width": 100 },
"xs": { "width": 300 },
"s": { "width": 600 },
"m": { "width": 800 },
"l": { "width": 1000 },
"xl": { "width": 2000 }
}
},
"scripts": {
"dev": "ghost-cli dev",
"zip": "zip -r custom-theme.zip . -x \"*.git*\" \"*.ds_store*\""
}
}
<!DOCTYPE html>
<html lang="{{@site.locale}}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{meta_title}}</title>
<link rel="stylesheet" type="text/css" href="{{asset "css/screen.css"}}" />
{{ghost_head}}
</head>
<body class="{{body_class}}">
<header class="site-header">
{{#if @site.logo}}
<a href="{{@site.url}}" class="site-logo"><img src="{{@site.logo}}" alt="{{@site.title}}" /></a>
{{else}}
<a href="{{@site.url}}" class="site-title">{{@site.title}}</a>
{{/if}}
{{navigation}}
</header>
<main class="site-main">
{{{body}}}
</main>
<footer class="site-footer">
<div class="footer-container">
{{> "newsletter-form"}}
<p>© {{date format="YYYY"}} {{@site.title}}. All rights reserved.</p>
</div>
</footer>
<script src="{{asset "js/main.js"}}"></script>
{{ghost_foot}}
</body>
</html>
{{!< default}}
<section class="hero-banner">
<h1>{{@site.title}}</h1>
<p>{{@site.description}}</p>
</section>
<section class="post-feed">
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{pagination}}
{{!< default}}
{{#post}}
<article class="article {{post_class}}">
<header class="article-header">
<div class="article-meta">
<time datetime="{{date format="YYYY-MM-DD"}}">{{date format="MMMM D, YYYY"}}</time>
{{#primary_tag}}
<span class="meta-separator">•</span>
<a href="{{url}}" style="color: {{accent_color}};">{{name}}</a>
{{/primary_tag}}
<span class="meta-separator">•</span>
<span>{{reading_time}}</span>
</div>
<h1 class="article-title">{{title}}</h1>
{{#if feature_image}}
<figure class="article-media">
<img src="{{img_url feature_image size="xl"}}" alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}" />
{{#if feature_image_caption}}
<figcaption>{{feature_image_caption}}</figcaption>
{{/if}}
</figure>
{{/if}}
</header>
<section class="article-content">
{{#if access}}
{{content}}
{{else}}
<div class="members-paywall">
<h3>This article is for paying subscribers only</h3>
<p>Sign up or log in to unlock full access to this post.</p>
<a href="#/portal/signup" class="btn btn-primary">Subscribe Now</a>
</div>
{{/if}}
</section>
<footer class="article-footer">
<div class="article-tags">
{{#foreach tags}}
<a href="{{url}}" class="tag-badge">{{name}}</a>
{{/foreach}}
</div>
</footer>
</article>
{{/post}}
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
{{#if feature_image}}
<img src="{{img_url feature_image size="xl"}}" alt="{{title}}" class="page-image" />
{{/if}}
</header>
<section class="page-content">
{{content}}
</section>
</article>
{{/post}}
{{!< default}}
{{#tag}}
<header class="tag-header" style="border-top: 4px solid {{#if accent_color}}{{accent_color}}{{else}}#38bdf8{{/if}};">
{{#if feature_image}}
<img src="{{img_url feature_image size="xl"}}" alt="{{name}}" class="tag-image" />
{{/if}}
<h1 class="tag-title">{{name}}</h1>
{{#if description}}
<p class="tag-description">{{description}}</p>
{{/if}}
<span class="tag-count">{{plural ../pagination.total empty='No posts' singular='% post' plural='% posts'}}</span>
</header>
{{/tag}}
<section class="post-feed">
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{pagination}}
{{!< default}}
{{#author}}
<header class="author-header">
{{#if profile_image}}
<img src="{{img_url profile_image size="s"}}" alt="{{name}}" class="author-avatar" />
{{/if}}
<h1 class="author-name">{{name}}</h1>
{{#if bio}}
<p class="author-bio">{{bio}}</p>
{{/if}}
<div class="author-meta">
{{#if location}}<span>📍 {{location}}</span>{{/if}}
{{#if website}}<a href="{{website}}" target="_blank">🌐 Website</a>{{/if}}
{{#if twitter}}<a href="{{twitter_url}}" target="_blank">🐦 Twitter</a>{{/if}}
</div>
</header>
{{/author}}
<section class="post-feed">
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{pagination}}
Reusable Theme Partials (`partials/*.hbs`)
<nav class="nav-menu">
<ul class="nav-list">
{{#foreach navigation}}
<li class="nav-item {{#if current}}nav-current{{/if}}">
<a href="{{url absolute="true"}}" class="nav-link">{{label}}</a>
</li>
{{/foreach}}
</ul>
</nav>
<article class="post-card {{post_class}}">
{{#if feature_image}}
<a href="{{url}}" class="post-card-media">
<img src="{{img_url feature_image size="m"}}" alt="{{title}}" loading="lazy" />
</a>
{{/if}}
<div class="post-card-body">
{{#primary_tag}}
<a href="{{url}}" class="post-card-tag" style="color: {{accent_color}};">{{name}}</a>
{{/primary_tag}}
<h2 class="post-card-title">
<a href="{{url}}">{{title}}</a>
</h2>
<p class="post-card-excerpt">{{excerpt words="25"}}</p>
<div class="post-card-footer">
<span class="post-card-author">{{primary_author.name}}</span>
<span class="post-card-date">{{date format="MMM D, YYYY"}}</span>
</div>
</div>
</article>
Store this image snippet inside your partials/ directory. You can inject it anywhere across any template using {{> "post-thumbnail"}}. Updating this single file will update the thumbnail component across your entire site.
{{#if feature_image}}
<a href="{{url}}" class="post-card-image-link" style="display: block; flex-shrink: 0; width: 80px; height: 50px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border-color, #2a364f);">
<img class="post-card-image"
src="{{img_url feature_image size="s"}}"
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
loading="lazy"
width="300"
height="100"
style="width: 100%; height: 100%; object-fit: cover;"
/>
</a>
{{/if}}
<div class="newsletter-card">
<h3>Subscribe to {{@site.title}}</h3>
<p>Get the latest posts delivered straight to your inbox.</p>
<form data-members-form="subscribe" class="subscribe-form">
<div class="form-group">
<input data-members-email type="email" placeholder="yourname@domain.com" required class="form-input" />
<button type="submit" class="btn btn-primary">
<span class="button-content">Subscribe</span>
<span class="button-loader">...</span>
</button>
</div>
<div class="message-success">Great! Check your inbox to confirm your subscription.</div>
<div class="message-error">Please enter a valid email address.</div>
</form>
</div>
:root {
--brand-color: #38bdf8;
--bg-main: #080a0f;
--text-color: #f8fafc;
--card-bg: #121824;
}
body {
background-color: var(--bg-main);
color: var(--text-color);
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
margin: 0;
}
.site-header { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 2rem; }
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; padding: 2rem; }
.post-card { background: var(--card-bg); border-radius: 8px; overflow: hidden; }
.post-card img { width: 100%; height: 200px; object-fit: cover; }
.post-card-body { padding: 1.5rem; }
document.addEventListener('DOMContentLoaded', () => {
console.log('Ghost custom theme initialized successfully.');
// Simple smooth scroll handler for portal triggers
const portalLinks = document.querySelectorAll('a[href^="#/portal"]');
portalLinks.forEach(link => {
link.addEventListener('click', (e) => {
console.log('Ghost Portal trigger activated:', link.getAttribute('href'));
});
});
});
Custom Templates (`custom-*.hbs`)
Ghost automatically registers any top-level theme file starting with custom- in the Ghost Admin panel. Content editors can select these from the Template dropdown in the post/page settings sidebar.
A clean, focused template containing only the base header, post/page content body, and footer without sidebars or feed clutter.
{{!< default}}
{{#post}}
<article class="custom-blank-page">
<header class="blank-header">
<h1 class="blank-title">{{title}}</h1>
</header>
<div class="blank-body">
{{content}}
</div>
</article>
{{/post}}
Fetches 10 latest blog posts matching page tags, injecting the modular {{> "post-thumbnail"}} partial for optimized 80x50px cover thumbnails alongside article titles and metadata.
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
</header>
<section class="page-content">
{{content}}
</section>
{{! Fetch 10 latest blog posts using current page tags }}
{{#get "posts" limit="10" filter="tags:[{{tags visibility="all" autolink="false" separator=","}}]+id:-{{id}}" include="tags,authors"}}
<section class="blog10-image-section" style="margin-top: 2rem;">
<h2>Latest 10 Blog Posts</h2>
<div class="compact-post-list" style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1.5rem;">
{{#foreach posts}}
<article class="compact-post-item" style="display: flex; align-items: center; gap: 1rem; background: var(--card-bg, #121824); padding: 0.8rem; border-radius: 8px; border: 1px solid var(--surface-border, #1e293b);">
{{! REUSABLE THUMBNAIL SNIPPET INJECTION }}
{{> "post-thumbnail"}}
<div class="compact-post-details" style="flex-grow: 1;">
<h3 class="compact-post-title" style="margin: 0; font-size: 1rem;">
<a href="{{url}}" style="text-decoration: none; color: inherit;">{{title}}</a>
</h3>
<div class="compact-post-meta" style="font-size: 0.8rem; color: var(--text-muted, #64748b); margin-top: 0.2rem;">
<time datetime="{{date format="YYYY-MM-DD"}}">{{date format="MMM D, YYYY"}}</time>
{{#primary_tag}}
<span> • </span>
<span style="color: {{accent_color}};">{{name}}</span>
{{/primary_tag}}
</div>
</div>
</article>
{{/foreach}}
</div>
</section>
{{/get}}
</article>
{{/post}}
Displays the page content, then dynamically queries and outputs the 10 latest blog posts matching any tag assigned to this page.
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
</header>
<section class="page-content">
{{content}}
</section>
{{! Query 10 latest posts matching current page tags }}
{{#get "posts" limit="10" filter="tags:[{{tags visibility="all" autolink="false" separator=","}}]+id:-{{id}}" include="tags,authors"}}
<section class="dynamic-posts-section">
<h2>Latest 10 Related Posts</h2>
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{/get}}
</article>
{{/post}}
Ideal for landing hubs or topic indexes. Displays page content and pulls the 20 latest posts matching the page's tags.
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
</header>
<section class="page-content">
{{content}}
</section>
{{! Query 20 latest posts matching current page tags }}
{{#get "posts" limit="20" filter="tags:[{{tags visibility="all" autolink="false" separator=","}}]+id:-{{id}}" include="tags,authors"}}
<section class="dynamic-posts-section">
<h2>Latest 20 Tagged Articles</h2>
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{/get}}
</article>
{{/post}}
Fetches 10 posts scoped specifically to the page's primary tag, providing explicit context-driven post lists.
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
</header>
<section class="page-content">
{{content}}
</section>
{{#primary_tag}}
{{#get "posts" limit="10" filter="tag:{{slug}}+id:-{{../id}}" include="tags,authors"}}
<section class="tag-posts-section">
<h2>10 Latest Posts in "{{../name}}"</h2>
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{/get}}
{{/primary_tag}}
</article>
{{/post}}
Displays the page content followed by a 10-post recommendation stream targeting similar categories while excluding the current page ID.
{{!< default}}
{{#post}}
<article class="page-container">
<header class="page-header">
<h1>{{title}}</h1>
</header>
<section class="page-content">
{{content}}
</section>
{{#if primary_tag}}
{{#get "posts" limit="10" filter="tag:{{primary_tag.slug}}+id:-{{id}}" include="tags,authors"}}
<section class="similar-posts-section">
<h2>Similar Posts You Might Like</h2>
<div class="post-grid">
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</section>
{{/get}}
{{/if}}
</article>
{{/post}}
The routes.yaml file is Ghost's master routing configuration engine. It allows developers to break away from the default blog structure and build sophisticated web publications, podcasts, knowledge bases, and multi-channel media hubs.
1. `routes:` (Custom Pages)
Maps specific URL paths directly to dedicated Handlebars template files without requiring a page in the Ghost Admin.
2. `collections:` (Post Streams)
Divides your posts into distinct index feeds (e.g., separating /blog/ posts from /podcast/ episodes or /news/).
3. `taxonomies:` (Category Slugs)
Customizes the URL prefix for tags and author archives (e.g., changing /tag/tech/ to /topic/tech/).
4. `channels:` (Filtered Feeds)
Creates sliced post streams without removing posts from the primary collections feed.
routes:
/about/:
template: page-about
data: page.about
/subscribe/:
template: subscribe
/landing/:
template: landing-page
collections:
/podcast/:
permalink: /podcast/{slug}/
template: podcast-index
filter: tag:hash-podcast
data: page.podcast-info
/journal/:
permalink: /journal/{slug}/
template: index
filter: tag:-hash-podcast
taxonomies:
tag: /topic/{slug}/
author: /writer/{slug}/
Key Breakdown of the Real-Life Example above:
- Static Route Injection:
/subscribe/usessubscribe.hbswhile/about/binds directly to a specific Ghost Admin page context (page.about). - Content Splitting: Posts tagged with internal tag
#podcastare stripped out of the main/journal/feed (viatag:-hash-podcast) and routed into their own isolated collection at/podcast/ep-title/. - SEO Taxonomy Prefixes: Rewrites default Ghost URL patterns so that
yourdomain.com/tag/tech/becomesyourdomain.com/topic/tech/and authors appear at/writer/name/.
Ghost Theme Tags Documentation
Official developer guide on Handlebars data tags and context helpers.
docs.ghost.org/themes/helpers/data/tagsGhost Help Center: Tag Management
User guide on organizing publications using public and internal tags.
ghost.org/help/tagsAspire Themes: Ghost Tag Pages Guide
Deep-dive article on building custom tag pages and advanced filtering.
aspirethemes.com/blog/ghost-tags-pageSpectral Web Services: Tag Organization
Best practices for structuring content taxonomies in production Ghost sites.
spectralwebservices.com/blog/...Ghost Content Organization Blueprint
Official guide on content hierarchy, collections, and routing strategies.
ghost.org/resources/content-organization