5/30/18 - There's a Catch: Earnie Smith(@ShortFormErnie) pointed out that this doesn't work in all views. Specifically RSS and AMP. After digging around the Ghost source, it looks like AMP - at the very least - will require modifying the built-in AMP app that comes with Ghost. I'm still looking into it.

Ghost Apps are basically like Wordpress plugins. Only thing is, there is no stable API or platform for them yet. You should keep that in mind moving forward.

Just want to get at the code? It lives here:

You can do other stuff with Ghost Apps, but we're going to focus on filters. As of the time of writing(11/5/17), Ghost fires 3 different types of filters when a post is loaded.

ghost_head - Gets passed an array of items which you are expected to return to get join-ed together and put inside your <head> tags.
ghost_foot - Gets passed an array of items which you are expected to return to get join-ed together an inserted just before your closing </body> tag.
prePostsRender - Gets an object passed that contains everything about your post. Today, we're going to modify and return this object.

A Basic App

Let's start with something easy. We'll just make a simple filter app that turns [awesome] into something... fancy.


{ "name": "my-awesome-app"
, "description": "This Ghost app is awesome"
, "version": "0.0.1"
, "license": "MIT"
, "dependencies": {
  "ghost-app": "0.0.2"
, "ghost": {
  "permissions": {
    "filters": ["prePostsRender"]

Most of this should be pretty familiar. We named and described our app, and then we added dependencies. You may need more for your app, but you do need "ghost-app".

The part you might not recognize, is the "ghost" section. We have to explicitly give our app permission to do... well... anything. In this case, run filters on the "prePostsRender" event.


var App = require('ghost-app')
,   MyAwesomeApp;

MyAwesomeApp = App.extend({
  filters: {
    prePostsRender: 'handlePrePostsRender'
, handlePrePostsRender: function handlePrePostsRender(payload) {
    payload.html = payload.html.replace(
    , '<span style="color:darkorange;">Awesome!</span>'
    return payload;

module.exports = MyAwesomeApp;

So, what did we do here? First we add our method to Ghost's list of filters for the prePostsRender event, which gets fired every time a post/page is rendered.

A word of warning. Ghost doesn't execute filters in any particular order. You can't rely on chaining the output of 2 apps together in sequence.

Next, we define our method to pass our posts through. It will take a payload object that contains pretty much everything about our post. Its slug, title, etc... Most importantly(for this instance), its rendered HTML.

Inside our method, we just do a simple .replace to find [awesome] and replace it with a <span>. But why did we use regex? We could have just put '[awesome]' in for the search parameter and it would have worked, right? Yeah, but it would have only found and replaced the first occurence. Using regex, we can search for a specific string, find arguments(which we'll do next time), and - using the g flag - tell .replace to find and replace every instance.

Finally, we return the modified payload. If you don't return the payload, then Ghost will just throw an error. It's kind-of a big deal.

Trying it out

Now it's time to install the app. I'm going to assume you have ghost up and running somewhere, but if you still need to do that, you can find more at https://ghost.org.

Put your files in a new directory in your Ghost install's apps directory(./content/apps). We'll call it my-awesome-app.


Then npm install(or yarn or whatever you use) to pull in your deps.

Next, update your database to tell ghost that we want to use the app. Use whatever tools you're most comfortable with to do this:

UPDATE `settings` SET `value` = '["my-awesome-app"]' WHERE `key` = 'active_apps'

Note - as of writing, the current master branch of Ghost uses active_apps, but it may be stored as activeApps depending on what version of Ghost you're using.

With that done, (re)start your Ghost install. It should find your app and load it up. Now you can edit or start a post and, anywhere you put [awesome] into your markdown, Ghost will automatically turn it into our HTML.

Turning this:

Into this:

There's a gotcha though. Navigate to your Ghost home page and you'll find that it just sort-of hangs. What!? No! Why?!

Turns out, prePostsRender fires on the home page too... just without an html attribute on the payload. This causes errors. Let's account for that.

Kill your Ghost server and open up your index.js. Just inside your handle function put the following:

if (!payload.html) return payload;

That will just quietly move along if the handler is called for a view that doesn't directly render markdown to HTML.

Now, save your file and restart your Ghost server. Congratulations! You just bastardized your first shortcode into a Ghost install! Also, did you spot the second issue?

Up Next: Adding Arguments to Our Ghost Shortcodes

And, maybe thinking about fixing that issue we left hanging