Ghost has come a long way since I wrote my first post on safely creating custom Handlebars helpers, but unfortunately not far enough that it's an easy feat. Whilst there's some movement in enabling developers to add Handlebars helpers via Ghost Apps, it's still a pain to get working (I was unable to), and requires poking about inside your database.

In the last couple of weeks, I've been working quietly on this site - very little has changed, except that I wanted to cache some of the icons and charts on the Projects page. The main charts are driven by WakaTime, which provides an embed tag to display it inline in your own webpages. This is great, but it meant that rendering didn't appear particularly smooth (as my content after would render first, and then jump lower as the embed came in). I decided that I wanted a way to render "cached" assets in my site; something I'd wanted for a long time to inline my images, and so on. Of course, to get these cached assets from the backend to the browser, I'd need Handlebars to assist. It turns out that writing Handlebars helpers is fairly easy in Ghost 1.x, at least in the versions I've used.

Writing Your Helpers

Ghost helpers loaded in the way detailed below have to be written in a certain way; I'm fairly certain they're backed by express-hbs so check out the documentation there - but for brevity, here's a quick set of examples:

// purely synchronous context creation
function mySynchronousHelper(options) {
  return options.fn({ test: true });
}

// fire a callback which provides your context
function myAsynchronousHelper(options, callback) {
  callback(options.fn({ test: true }));
}

// return a Promise which resolves to your context
function myAsynchronousPromiseHelper(options) {
  return new Promise(function (resolve, reject) {
    resolve(options.fn({ test: true }));
  });
}

You can define your helpers as you want to, really, but in my case I've been following the pattern of one helper per file (as that's what Ghost core appears to do). Just remember to export your helpers! I had no idea that asynchronous helpers were supported; this was an excellent surprise as I potentially needed to do web requests and/or filesystem operations to retrieve my assets.

Attaching Your Helpers

This is where it's a little different to how I used to recommend it be done; Ghost stores all of the internal helpers inside core/server/helpers, and an index of them inside core/server/helpers/index.js. Your helpers need to be added here in order for them to be included inside your Ghost runtime correctly. It should be noted that when using the Ghost CLI, you will have to place inside current/core/server/helpers and repeat these steps each and every time your update your Ghost installation. In my case I had a helper named fetch, which looked something like this inside of a fetch.js file:

// require any external modules
const request = require('request');

// export the asynchronous fetch helper
module.exports = function fetch(options, callback) {
  // do some stuff
};

To activate this helper inside Ghost, you have to copy fetch.js to core/server/helpers/fetch.js (this isn't stricly necessary, but makes the next step easier). Once done, you need to add an entry to core/server/helpers/index.js to attach your helper. This is fairly obvious how to do - you can copy the way it's done for all of the internal Ghost helpers. The only thing to note is the importance of picking the correct registration method; make sure you pick the correct one based on the implementation of your helper. In my case above, I would have to use registerAsyncThemeHelper due to my use of the callback. Any synchronous code can safely use registerThemeHelper.

In the example I showed above, I made use of an external module from npm by using request. You need to ensure that Ghost has these modules installed before starting Ghost, otherwise everything will just crash. In the latest versions of Ghost, yarn is used to install modules so simply yarn add request was sufficient in my case (from inside the core/ directory). If you're on older Ghost versions it might be that they use npm still, so in that case npm install request would be what you want. Don't forget this step! Ghost will start and appear to be running correctly until you try load a page with your helper, which will then crash Ghost.

Production Validation

Doing the above is sufficient for development, but not production (much to my amusement when I tried to ship it). Ghost enables certain validations when configured for production which are not enabled for development, and so your theme won't start correctly. This is mainly due to their gscan module, which includes a set of accepted helpers. Fortunately, it's simple enough to add your helper as recognised.

This step will vary based on the gscan version shipped with your Ghost setup. In the latest (at the time of writing), you will need to edit the file core/node_modules/gscan/lib/spec.js, but in their master branch it appears to be core/node_modules/gscan/lib/specs/v1.js (probably due to the upcoming Ghost v2.x). Right at the top of these files is a knownHelpers variable, which is an array containing the names of accepted helpers. Add your helper name inside this array, restart Ghost, and everything should be good to go.