This post covers how to customise tag and post paths with the Ghost blogging platform.

The background required is a working installation of Ghost 0.4.2, a basic understanding of JavaScript and a decent text editor, say Sublime 3.

Motivation

Firstly, why? It’s not an especially useful trick, but as I host my blog at andy.stanton.is/writing I wanted to have blog posts like

writing/about/custom-post-tag-paths-with-ghost

and tags like

writing/on-the-subject-of/video

Although there are alternative approaches (for example url rewriting with nginx or Apache), a secondary motivation is that I’ve been experimenting with NodeJS lately and wanted to dissect a production JavaScript codebase to see what makes it tick, or even whether I could make head or tail of it at all.

SPOILER ALERT: I could.

Before we start

Stop your Ghost blog and back it up. Please note this guide is provided as is and I accept no liability if your blog stops working.

I will use my blog’s example of changing the tag path from /tag to /on-the-subject-of and prefixing the path of blog posts with /about.

Tags

We’ll start with tags. The files that we will modify are:

  • core/server/config/url.js
  • core/server/routes/frontend.js
  • core/server/controllers/frontend.js
  • core/server/helpers/index.js

Update core/server/config/url.js

This module contains the code that Ghost uses to construct the path for a particular item (post, tag, page etc). We are interested in this because we want to change links to tags from being created as tag/some_tag to on-the-subject-of/some_tag.

Within the function urlFor(context, data, absolute) locate the block:

// trying to create a url for an object
if (context === 'post' && data.post && data.permalinks) {
    urlPath = urlPathForPost(data.post, data.permalinks);
} else if (context === 'tag' && data.tag) {
    urlPath = '/tag/' + data.tag.slug + '/';
}

Modify the existing tag path from

urlPath = '/tag/' + data.tag.slug + '/';

to your new desired tag path, e.g.

urlPath = '/on-the-subject-of/' + data.tag.slug + '/';

Update core/server/routes/frontend.js

This module is responsible for the routing of requests to the blog to the correct controller. This means that when someone hits your blog with the path /rss or /tag/tag_name, then the rss feed or the posts corresponding to that tag will get returned.

We’re going to modify it so that our paths containing our replacement for the word tag are now responsible for invoking the appropriate tag controllers.

Locate the section where the tags are mapped to controllers:

server.get('/tag/:slug/rss/', frontend.rss);
server.get('/tag/:slug/rss/:page/', frontend.rss);
server.get('/tag/:slug/page/:page/', frontend.tag);
server.get('/tag/:slug/', frontend.tag);

Replace tag with your preferred path e.g. on-the-subject-of.

Update core/server/controllers/frontend.js

This module contains the frontend controller configuration for Ghost. We need to modify this because Ghost performs some of its url construction here for the rss and paginated views of tags. This is important because otherwise you end up with writing/tag/coding/page/2 rather than writing/on-the-subject-of/coding/page/2.

There’s a lot of replacing to do here, but unfortunately there is no blanket approach - this has to be done by hand.

Within the function function tagUrl(tag, page) change the constructed url from:

var url = config().paths.subdir + '/tag/' + tag + '/';

to

var url = config().paths.subdir + '/on-the-subject-of/' + tag + '/';

Next find the rss controller function: 'rss': function (req, res, next) and update the if condition that begins if (isNaN(pageParam) || pageParam < 1 || from:

req.route.path === '/tag/:slug/rss/:page/'

to

req.route.path === '/on-the-subject-of/:slug/rss/:page/'

Then within this condition, replace:

return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/');

with

return res.redirect(config().paths.subdir + '/on-the-subject-of/' + tagParam + '/rss/');

Slightly further down replace:

feedUrl = feedUrl + 'tag/' + page.aspect.tag.slug + '/';

with

feedUrl = feedUrl + 'on-the-subject-of/' + page.aspect.tag.slug + '/';

and

return res.redirect(config().paths.subdir + '/tag/' + tagParam + '/rss/' + maxPage + '/');

with

return res.redirect(config().paths.subdir + '/on-the-subject-of/' + tagParam + '/rss/' + maxPage + '/');

Update core/server/helpers/index.js

The Core Helpers module appears to be dedicated to utility methods for developers creating themes. We’re going to update a function used to retrieve the current page url in the context of a tag.

Within the page_url function: coreHelpers.page_url = function (context, block), change:

url += '/tag/' + this.tagSlug;

to

url += '/on-the-subject-of/' + this.tagSlug;

That’s tags done! You can test it out at this point by starting up your blog and verifying that links to tags are now mapped to your new path.

Posts

Posts are a little different to tags as we’re introducing a new section of the url path rather than replacing an existing one, but we will be revisiting some of the files we touched in the previous section.

Make sure you stop Ghost again before you continue with this step.

The files we will be modifying for posts are:

  • core/server/config/url.js
  • core/server/routes/frontend.js
  • core/server/controllers/frontend.js

Update core/server/config/url.js

We updated this file for the tags earlier. This time we want to change links to posts from being created as some_post to about/some_post.

Within the function urlPathForPost(post, permalinks), update the base path from being the empty string by changing

var output = '',

to your new desired post path, e.g.

var output = '/about',

Update core/server/routes/frontend.js

This file got updated earlier too. We want to change it in a similar way and make sure the posts controller is invoked for requests addressed to about/*.

At the very bottom, find the line:

server.get('/*', frontend.single);

Add your custom post path before the /* as follows:

server.get('/about/*', frontend.single);

Update core/server/controllers/frontend.js

We updated this earlier. This time we need to modify it because Ghost can only process posts based on the url format it understands, but since we’ve changed the url format we need to help Ghost out.

Within the ‘single’ entry of the frontendControllers object: 'single': function (req, res, next) {

Modify the path variable, removing the custom post path. You can do this by replacing occurrences of your path with the empty string e.g.

var path = req.path,

becomes

var path = req.path.replace("/about", ""),

You can now verify the change has succeeded by firing up Ghost and verifying that links to posts are mapped to your new path. And that’s it!

Conclusions

This experiment was a fun way to have a look around the Ghost source and gain a bit of experience navigating a JavaScript codebase. It would be nice to make this more than a hack and make it customisable from the core Ghost configuration, but that’s a project for another day!