Skip to content

Ghost Handlebars Templates — foreach, if, Helpers and Contexts

DodaTech Updated 2026-06-28 9 min read

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 posts
  • pages — Published pages
  • tags — All public tags
  • authors — All authors
  • tiers — Membership tiers
  • products — 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

  1. 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.

  2. 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.

  3. 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}}.

  4. Accessing properties outside their context: primary_author.name is only available inside a post context. Trying to access it in index.hbs outside a foreach loop returns nothing.

  5. 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

  1. 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.

  2. 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.

  3. What does the @ prefix mean in Ghost Handlebars? Answer: Properties with the @ prefix access Ghost system data rather than content data. @site gives site-level settings (title, url, description). @member gives the current logged-in member's data.

  4. 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 @member conditional content.

FAQ

Can I create custom Handlebars helpers in my theme?

Ghost does not support custom Handlebars helpers in themes. You are limited to Ghost's built-in helpers. For custom logic, use the Ghost API or develop a custom integration.

Why is my {{#get}} query returning no results?

Check the filter syntax. Common issues: incorrect field names (use 'featured' not 'isFeatured'), wrong value types (booleans use 'true' not 'yes'), or mismatched tag names. Use the Ghost admin to verify the data exists.

How do I debug Handlebars template issues?

Enable Ghost's developer experiments for verbose error messages. Use the {{log}} helper: {{log title}} outputs the value to the browser console. Also check the Ghost logs at content/logs/ for rendering errors.

Can I use JavaScript logic in my theme?

JavaScript in your theme's JS files runs client-side in the browser and is not related to Handlebars. You can manipulate the DOM with JavaScript, but you cannot use JavaScript logic inside Handlebars templates.

What is the maximum recursion depth for {{#get}} queries?

Ghost limits nested {{#get}} queries to prevent infinite loops. You can typically nest 2-3 levels. For complex data needs, consider using the Ghost API instead of template-level queries.

Mini Project

Your task: Create a set of advanced Handlebars templates for a magazine-style Ghost site.

  1. 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.
  2. Create a post card partial that accepts post data and renders a consistent card layout.
  3. Create a tag filter section that queries posts by a specific tag.
  4. Add a member-only section that shows different content based on @member.paid.
  5. Use loop variables (@first, @last, @even) to add alternating styles.
  6. 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:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro