How Ghost Frontend Helpers Work: A Complete Guide to Creating Custom Handlebars Helpers

Ghost uses a centralized registry system to manage Handlebars helpers, exposing ghost.helperService methods that allow apps to register synchronous or asynchronous custom helpers individually or by directory.

Ghost's theming engine relies on Handlebars to render dynamic content, providing a robust set of built-in helpers like {{title}} and {{url}}. The platform exposes a clean API through ghost.helperService that enables developers to extend functionality by creating custom Handlebars helpers. Understanding this architecture allows you to safely modify template behavior without hacking core files.

Core Helper Architecture

The helper system is built around three specialized modules in ghost/core/core/frontend/services/helpers/:

  • registry.js – Maintains an in-memory map of helper names to functions and handles directory scanning logic.
  • handlebars.js – A thin wrapper that forwards registration calls to the global Handlebars instance used by Ghost.
  • index.js – The public façade exposed as ghost.helperService, supplying registerHelper, registerAlias, registerDir, and an init routine.

According to the TryGhost/Ghost source code, this separation of concerns ensures that the registration API remains stable while the underlying Handlebars integration can evolve independently.

How Ghost Loads Built-in Helpers

When the Ghost frontend starts, the helper initialization follows a strict sequence:

  1. ghost.helperService.init() is called, which builds the absolute path to core/frontend/helpers.
  2. registry.registerDir scans the directory using globSync('!(index).js') to exclude index files.
  3. Each helper file is required, and registerHelper stores the function in the internal registry object.
  4. Registration detects async capabilities: if helperFn.async === true, Ghost calls handlebars.registerAsyncThemeHelper; otherwise it uses handlebars.registerThemeHelper.

The handlebars.js wrapper delegates to the global instance with minimal overhead:

module.exports.registerThemeHelper = function registerThemeHelper(name, fn) {
    hbs.registerHelper(name, fn);
};

module.exports.registerAsyncThemeHelper = function registerAsyncThemeHelper(name, fn) {
    asyncHelperWrapper(hbs, name, fn);
};

Creating Custom Handlebars Helpers

You can add custom helpers through the ghost.helperService API using two distinct approaches.

Register a Single Helper

Call ghost.helperService.registerHelper(name, fn) from any app's activation code to register individual functions:

// app activation code
ghost.helperService.registerHelper('myHelper', function(options) {
    // `this` represents the template context
    const name = (this.author && this.author.name) || 'Guest';
    return `Welcome, ${name}!`;
});

Register a Directory of Helpers

Place multiple helper files in a folder and call ghost.helperService.registerDir(path) to auto-load them:

// my-app/index.js
const path = require('path');

module.exports = {
    activate(ghost) {
        ghost.helperService.registerDir(
            path.resolve(__dirname, './lib/helpers')
        );
    }
};

Each .js file in the directory should export a single function. Ghost automatically derives the helper name from the filename (e.g., my-helper.js becomes {{my-helper}}).

Creating Async Helpers

Ghost automatically detects async functions and wraps them for safe template usage. Simply export an async function or set fn.async = true:

// Async helper that fetches data
ghost.helperService.registerHelper('fetchAuthor', async function(options) {
    // this refers to the post object
    const author = await this.author();
    return author.name;
});

Because the function is async, Ghost registers it via registerAsyncThemeHelper, allowing the template to render without blocking while the promise resolves.

Built-in Helper Examples

Ghost ships with numerous built-in helpers located in ghost/core/core/frontend/helpers/. For example, title.js returns a safely escaped string:

const {SafeString} = require('../services/handlebars');

module.exports = function title(options) {
    const title = this.title || '';
    return new SafeString(title);
};

The private-blogging app (ghost/core/apps/private-blogging/index.js) demonstrates real-world usage of registerDir to inject authentication-related helpers.

Summary

  • Ghost maintains a registry-based architecture that decouples helper storage from Handlebars registration.
  • Built-in helpers auto-load from core/frontend/helpers via globSync pattern matching.
  • Apps register custom helpers via registerHelper (single) or registerDir (bulk), accessible through ghost.helperService.
  • Async helpers are automatically detected by the async property or Promise return type and wrapped for non-blocking template execution.
  • Custom helpers become immediately available in themes as {{helperName}} once registered during app activation.

Frequently Asked Questions

How do I register a custom helper in Ghost?

Register a custom helper by calling ghost.helperService.registerHelper(name, func) inside your app's activate method. Alternatively, use registerDir(path) to load an entire folder of helper files automatically.

What is the difference between sync and async helpers in Ghost?

Synchronous helpers execute immediately and return strings or SafeString objects. Async helpers return Promises and are wrapped by Ghost's asyncHelperWrapper to prevent blocking template rendering while data loads.

Where should I place custom helper files in a Ghost app?

Store helper files in a subdirectory of your app (commonly lib/helpers/), ensuring each file exports a single function. Ghost derives the helper name from the filename, converting dashes to camelCase where appropriate.

Can themes register custom helpers or only apps?

Only apps can register custom helpers using ghost.helperService. Themes cannot directly register helpers; they must rely on built-in helpers or helpers provided by active apps installed on the Ghost instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →