Ghost Handlebars Templates — foreach, if, Helpers and Contexts
In this tutorial, you'll learn how to use Handlebars templating in Ghost themes — mastering the foreach helper, if/else conditionals, Ghost-specific helpers, template contexts, and data access patterns for building dynamic templates.
What You'll Learn
- Handlebars syntax basics: expressions, helpers, block helpers
- The foreach helper for iterating over posts, tags, and authors
- The if/else helper for conditional content
- Ghost-specific helpers: content, excerpt, date, img_url, reading_time
- Template contexts: post, page, tag, author, site, and @site
- Accessing nested data and properties
- The {{#get}} helper for custom data queries
- Pagination and the {{pagination}} helper
- Using {{#has}} for feature checking
Why It Matters
Handlebars is the template engine that powers every Ghost theme. It is intentionally simple — logic-less templates that keep presentation separate from data. But simple does not mean limited. Ghost extends Handlebars with powerful helpers designed specifically for publishing: iterating over posts, displaying excerpts, generating image URLs, querying custom data, and more. Mastering these helpers lets you build any layout you can imagine without learning a complex framework.
Real-World Use
A theme developer wants to create a magazine-style homepage with three sections: a featured hero post, a grid of recent articles, and a sidebar with popular tags. She uses {{#foreach}} with the limit parameter for the hero, again with pagination for the grid, and {{#get "tags"}} for the sidebar. The entire homepage template is under 50 lines of Handlebars.
Learning Path
flowchart LR A["Theme Basics"] --> B["Handlebars Templates
You are here"]:::current B --> C["Theme Assets"] C --> D["Custom Themes"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Handlebars Syntax
Handlebars uses double curly braces {{ }} to output data and {{# }} {{/ }} for block helpers.
Expressions
<!-- Output a variable -->
<h1>{{title}}</h1>
<!-- Output with HTML escaping (safe) -->
<p>{{excerpt}}</p>
<!-- Output without HTML escaping (use for trusted HTML) -->
<section>{{{body}}}</section>
Three curly braces ({{{ }}}) output raw HTML without escaping. Use this only for content you trust — post body, custom HTML, etc. Two curly braces escape HTML entities.
Dot Notation
Access nested properties with dot notation:
<!-- Author inside post context -->
<span>{{primary_author.name}}</span>
<!-- Nested settings -->
<a href="{{@site.url}}">{{@site.title}}</a>
<!-- Image URL with transformation -->
<img src="{{img_url feature_image size="l"}}">
The foreach Helper
{{#foreach}} iterates over arrays like posts, tags, and authors.
Basic Usage
{{#foreach posts}}
<article>
<h2><a href="{{url}}">{{title}}</a></h2>
<p>{{excerpt words="20"}}</p>
</article>
{{/foreach}}
Loop Variables
Inside a {{#foreach}} block, Ghost provides loop control variables:
| Variable | Description |
|---|---|
| @index | Zero-based index of the current item |
| @number | One-based index of the current item |
| @first | True if this is the first item |
| @last | True if this is the last item |
| @even | True if the index is even |
| @odd | True if the index is odd |
{{#foreach posts}}
<article class="{{#if @even}}even{{else}}odd{{/if}}">
<span class="post-number">{{@number}}</span>
<h2><a href="{{url}}">{{title}}</a></h2>
</article>
{{/foreach}}
Limit and Visibility
The foreach helper accepts parameters for controlling iteration:
<!-- Show only 3 posts -->
{{#foreach posts limit="3"}}
<!-- Skip first post (for featured hero layout) -->
{{#foreach posts from="2"}}
<!-- Show only public tags (exclude internal tags) -->
{{#foreach tags visibility="public"}}
The if/else Helper
Use {{#if}} to conditionally display content.
{{#if feature_image}}
<img src="{{img_url feature_image}}">
{{/if}}
{{#if featured}}
<span class="featured-badge">Featured</span>
{{/if}}
Inverse Condition
{{#if feature_image}}
<img src="{{img_url feature_image}}">
{{else}}
<div class="placeholder-image">No image</div>
{{/if}}
Unless
{{#unless}} is the opposite of {{#if}}:
{{#unless access}}
<div class="signup-prompt">Sign up to read this post</div>
{{/unless}}
Ghost-Specific Helpers
Ghost provides helpers designed specifically for publishing content.
content
Outputs the full post body or a trimmed version:
<!-- Full content -->
<section>{{content}}</section>
<!-- First 100 words (for excerpts) -->
<section>{{content words="100"}}</section>
excerpt
Returns the post excerpt (custom excerpt if set, otherwise auto-generated):
<p>{{excerpt}}</p>
<!-- Custom length -->
<p>{{excerpt words="25"}}</p>
date
Formats dates:
<!-- Default format -->
<time datetime="{{date}}">{{date}}</time>
<!-- Custom format -->
<time datetime="{{date format="YYYY-MM-DD"}}">{{date format="MMMM DD, YYYY"}}</time>
<!-- Time ago -->
<span>{{date timeago="true"}}</span>
img_url
Generates optimized image URLs with size variants:
<!-- Default size (original) -->
<img src="{{img_url feature_image}}">
<!-- Specific size -->
<img src="{{img_url feature_image size="l"}}">
<!-- Custom width -->
<img src="{{img_url feature_image width="800"}}">
<!-- WebP format -->
<img src="{{img_url feature_image format="webp"}}">
reading_time
Shows estimated reading time:
<span>{{reading_time}}</span>
<!-- Output: "5 min read" -->
meta_title and meta_description
Outputs SEO-optimized metadata:
<title>{{meta_title}}</title>
<meta name="description" content="{{meta_description}}">
pagination
Renders pagination links for post listings:
{{pagination}}
<!-- With custom classes -->
{{pagination class="my-pagination"}}
ghost_head and ghost_foot
Injected by Ghost with metadata, scripts, and code injection:
{{ghost_head}}
<!-- ... page content ... -->
{{ghost_foot}}
The {{#get}} Helper
The {{#get}} helper queries the database for custom data collections. This is one of the most powerful Ghost template features.
<!-- Get 5 featured posts -->
{{#get "posts" filter="featured:true" limit="5"}}
<section class="featured-section">
<h2>Featured Posts</h2>
{{#foreach posts}}
<article>
<h3><a href="{{url}}">{{title}}</a></h3>
</article>
{{/foreach}}
</section>
{{/get}}
Filter Syntax
<!-- Simple filter -->
filter="featured:true"
<!-- Tag filter -->
filter="tags:[javascript, python]"
<!-- Date filter -->
filter="published_at:>2024-01-01"
<!-- Multiple filters -->
filter="featured:true+tags:[tutorial]"
<!-- Exclude -->
filter="tags:-[internal]"
Order and Pagination
{{#get "posts" filter="tags:[tutorial]" order="published_at desc" limit="6"}}
{{#foreach posts}}
...
{{/foreach}}
{{/get}}
Resource Types
The {{#get}} helper can query these resources:
posts— Published postspages— Published pagestags— All public tagsauthors— All authorstiers— Membership tiersproducts— Products (if using Ghost Commerce)
The {{#has}} Helper
The {{#has}} helper checks if the current context has specific properties:
<!-- Check for tags -->
{{#has tag="video"}}
<span class="video-badge">Video</span>
{{/has}}
<!-- Check for any of multiple tags -->
{{#has tag="featured, popular"}}
<span class="highlight">Popular</span>
{{/has}}
<!-- Check for visibility -->
{{#has visibility="paid"}}
<div class="paid-badge">Premium Content</div>
{{/has}}
Template Contexts
Each Ghost template has a default context — the data available to that template.
| Template | Default Context |
|---|---|
| index.hbs | Posts collection |
| post.hbs | Single post |
| page.hbs | Single page |
| tag.hbs | Tag + posts collection |
| author.hbs | Author + posts collection |
| error.hbs | Error details |
The @ Symbol
Properties prefixed with @ access Ghost-level data:
@site.title— Site title@site.url— Site URL@site.description— Site description@site.logo— Site logo URL@site.cover_image— Site cover image@site.locale— Site language@member— Current member (if logged in)@member.paid— Whether the member has paid subscription
{{#if @member}}
<p>Welcome back, {{@member.name}}!</p>
{{else}}
<a href="/signup/">Sign up</a>
{{/if}}
Common Mistakes
Using
{{content}}instead of{{{content}}}: The content helper must use three curly braces to render HTML without escaping. Using two curly braces shows raw HTML tags in the post body.Forgetting to close block helpers: Every
{{#foreach}},{{#if}}, and{{#get}}must have a matching{{/foreach}},{{/if}}, or{{/get}}. An unclosed helper breaks all templates below it.Using
{{#post}}in index.hbs: The index.hbs context is a collection of posts, not a single post. Inside{{#foreach posts}}, each post's properties are available directly — you do not need{{#post}}.Accessing properties outside their context:
primary_author.nameis only available inside a post context. Trying to access it in index.hbs outside a foreach loop returns nothing.Over-nesting helpers: Handlebars allows nesting, but deep nesting is confusing and hard to debug. Use partials to break complex layouts into manageable pieces.
Practice Questions
What is the difference between
{{content}}and{{excerpt}}? Answer:{{content}}outputs the full post body HTML.{{excerpt}}outputs a short plain-text summary (custom excerpt if set, otherwise auto-generated from the first paragraph). Use content for the full post page and excerpt for post listings.How do you query only featured posts in a Ghost theme? Answer: Use the
{{#get "posts" filter="featured:true"}}helper. This queries the database for all posts where the featured flag is true, independent of the current page context.What does the
@prefix mean in Ghost Handlebars? Answer: Properties with the@prefix access Ghost system data rather than content data.@sitegives site-level settings (title, url, description).@membergives the current logged-in member's data.Challenge: Build a Ghost theme template that uses at least 5 different helpers. Include: a
{{#get}}query for featured posts, a{{#foreach}}with loop variables, an{{#if}}conditional checking for feature_image, a{{#has}}check for tags, and@memberconditional content.
FAQ
Mini Project
Your task: Create a set of advanced Handlebars templates for a magazine-style Ghost site.
- Create a featured hero section using
{{#get "posts" filter="featured:true" limit="3"}}that shows the first post large and the next two as smaller cards. - Create a post card partial that accepts post data and renders a consistent card layout.
- Create a tag filter section that queries posts by a specific tag.
- Add a member-only section that shows different content based on
@member.paid. - Use loop variables (
@first,@last,@even) to add alternating styles. - Test all templates in a local Ghost installation.
This exercise gives you practical experience with every major Handlebars helper.
What's Next
Now that you understand Handlebars, learn how to manage theme assets:
Continue to Lesson 17: Theme Assets — CSS, JavaScript, package.json, and the asset pipeline.
Related lessons:
- Custom Themes — Theme structure and validation
- Theme Customization — Navigation, routes, dynamic routing
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro