Back to All Cheatsheet Libraries cheatsheets

Ghost Theme Blueprint

The master Ghost tag/helper dictionary, theme architecture and starter boilerplates, and routes.yaml reference for building custom Ghost themes.

Complete Ghost Handlebars Helper & Tag Reference
Real-time Search
Category Handlebars Tag / Helper Syntax & Parameters Primary Usage & Description
Content {{content}} {{content}} Outputs the primary post/page body HTML content inside single post templates[cite: 1].
Content {{title}} {{title}} Renders the plain text title of a post, page, author, or tag context.
Content {{excerpt}} {{excerpt words="30"}} Renders custom post excerpt or auto-generates snippet limited by words or characters.
Content {{reading_time}} {{reading_time minute="1 min read"}} Calculates estimated post reading duration automatically based on word count.
Content {{featured}} {{#if featured}}...{{/if}} Boolean evaluation check for posts flagged as "Featured" in the Ghost Admin.
Media {{feature_image}} {{#if feature_image}}...{{/if}} Returns absolute image URL for post or tag main cover image[cite: 1]. Used inside conditional checks[cite: 1].
Media {{img_url}} {{img_url feature_image size="m"}} Generates optimized responsive image URLs according to package.json sizes.
System {{ghost_head}} {{ghost_head}} Must be placed right before </head>. Injects SEO tags, Code Injection, and scripts.
System {{ghost_foot}} {{ghost_foot}} Must be placed right before </body>. Outputs admin bars, tracking, and footer scripts.
System {{body_class}} <body class="{{body_class}}"> Dynamic body CSS classes based on active template route (e.g., post-template, tag-tech).
System {{meta_title}} <title>{{meta_title}}</title> Generates context-aware page title for browser tabs and search engine indexing.
System {{@site}} {{@site.title}}, {{@site.logo}} Global site object containing publication title, description, logo, cover image, and settings.
Logic {{#foreach}} {{#foreach posts}}...{{/foreach}} Ghost loop helper for iterating over posts, tags, authors, or query collections.
Logic {{#get}} {{#get "posts" limit="3" filter="featured:true"}} Data fetching helper to query Content API for custom post collections anywhere in theme.
Logic {{#has}} {{#has tag="#newsletter"}}...{{/has}} Evaluates context conditions like specific tags, authors, or index positions.
Logic {{#is}} {{#is "post, page"}}...{{/is}} Checks current route context (e.g., home, post, page, tag, author).
Members {{#if @member}} {{#if @member}}...{{else}}...{{/if}} Renders gated UI elements depending on logged-in membership state.
Members {{#if access}} {{#if access}} {{content}} {{else}} {{/if}} Gating check evaluating if current visitor has subscription clearance to read content.
Members {{paywall}} {{paywall}} Outputs automatic subscription upgrade prompt card for non-members on gated posts.
Available Tag Data Attributes

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}}
Public Tags

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 (`#tagname`)

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}}
Complete Ghost Theme Directory Structure

Standard required and optional directory blueprint including core theme templates, partials, assets, and custom page templates:

my-custom-ghost-theme/ ├── assets/ │ ├── css/ │ │ └── screen.css │ └── js/ │ └── main.js ├── partials/ │ ├── navigation.hbs │ ├── newsletter-form.hbs │ ├── post-card.hbs │ └── post-thumbnail.hbs <-- Reusable Partial Snippet for feature thumbnails ├── author.hbs ├── custom-blank.hbs <-- Custom Template: Clean blank layout ├── custom-blog10-image.hbs <-- Custom Template: Page + 10 posts with reusable thumbnails ├── custom-dynamic-tags-10.hbs <-- Custom Template: Page + 10 latest tagged posts ├── custom-dynamic-tags-20.hbs <-- Custom Template: Page + 20 latest tagged posts ├── custom-similar-tags-10.hbs <-- Custom Template: Page + 10 similar category posts ├── custom-tag-posts.hbs <-- Custom Template: Page + 10 primary tag posts ├── default.hbs <-- Main wrapper base template (ghost_head / ghost_foot) ├── index.hbs <-- Main post collection index template ├── page.hbs <-- Single static page template ├── package.json <-- Theme configuration, image sizes & gscan settings ├── post.hbs <-- Single blog post template ├── routes.yaml <-- Custom routing & collection rules └── tag.hbs <-- Tag archive page template

Complete starter blueprints for every file in the theme architecture, plus specialized custom page templates.

Core Theme Files

`package.json`
Config
{ "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*\"" } }
`default.hbs`
Base Layout
<!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>&copy; {{date format="YYYY"}} {{@site.title}}. All rights reserved.</p> </div> </footer> <script src="{{asset "js/main.js"}}"></script> {{ghost_foot}} </body> </html>
`index.hbs`
Home Feed
{{!< 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}}
`post.hbs`
Single Post
{{!< 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">&bull;</span> <a href="{{url}}" style="color: {{accent_color}};">{{name}}</a> {{/primary_tag}} <span class="meta-separator">&bull;</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}}
`page.hbs`
Static Page
{{!< 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}}
`tag.hbs`
Tag Archive
{{!< 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}}
`author.hbs`
Author Profile
{{!< 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`)

`partials/navigation.hbs`
Nav Partial
<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>
`partials/post-card.hbs`
Card Partial
<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>
`partials/post-thumbnail.hbs`
Reusable Thumbnail Partial

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}}
`partials/newsletter-form.hbs`
Subscribe Form
<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>
`assets/css/screen.css`
CSS Stylesheet
: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; }
`assets/js/main.js`
JavaScript
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.

`custom-blank.hbs`
Blank Layout

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}}
`custom-blog10-image.hbs`
10 Posts + Thumbnail Partial

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> &bull; </span> <span style="color: {{accent_color}};">{{name}}</span> {{/primary_tag}} </div> </div> </article> {{/foreach}} </div> </section> {{/get}} </article> {{/post}}
`custom-dynamic-tags-10.hbs`
Dynamic 10 Posts

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}}
`custom-dynamic-tags-20.hbs`
Dynamic 20 Posts

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}}
`custom-tag-posts.hbs`
Primary Tag Feed

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}}
`custom-similar-tags-10.hbs`
Similar Category Feed

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}}
Deep-Dive: Understanding `routes.yaml`
Core Architecture

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.

Complete Real-Life Production `routes.yaml` Blueprint
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/ uses subscribe.hbs while /about/ binds directly to a specific Ghost Admin page context (page.about).
  • Content Splitting: Posts tagged with internal tag #podcast are stripped out of the main /journal/ feed (via tag:-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/ becomes yourdomain.com/topic/tech/ and authors appear at /writer/name/.
Official Documentation & Content Architecture Resources

Ghost Theme Tags Documentation

Official developer guide on Handlebars data tags and context helpers.

docs.ghost.org/themes/helpers/data/tags

Ghost Help Center: Tag Management

User guide on organizing publications using public and internal tags.

ghost.org/help/tags

Aspire Themes: Ghost Tag Pages Guide

Deep-dive article on building custom tag pages and advanced filtering.

aspirethemes.com/blog/ghost-tags-page

Spectral 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