Hacking a Routing Component in Jekyll

Initially I was going to title this article Hacking a routing component in Jekyll when hosted on GitHub Pages with Markdown, Liquid, and YAML. Obviously, that’s a bit long so I had to shorten it. Anyway, the point doesn’t change: Jekyll lacks some kind of router, and I think I found a not-so-terrible solution to mimic one.

But first things first: I come from a Symfony background, so by router I mean a component that maps URLs to names — a.k.a. routes – so you can safely change a URL without having to crawl your codebase to update all the links heading to the obsolete location.

Why would we need such a thing?

I am currently working hard on the docs for SassDoc v2 (not released yet, as of this writing). We have quite a bit of content; over 20 pages split across 4 different sections containing many code examples and cross references.

--ADVERTISEMENT--

A couple of times during the rewriting, I desired to change a URL. Problem is, when I change a URL I have to update all the links heading to this URL if I don’t want them to be broken. Crawling over 20 pages to make sure all URLs are alright is far from ideal…

This is why we need a routing component. Then we would refer to URLs by name rather than by their path, allowing us to change a path while keeping the name perfectly valid.

How does it work?

So what do we need to make this work? If you are running Jekyll but are not restricted to safe mode (which is unfortunately the case when using GitHub Pages for hosting), you can surely find/build a Ruby plugin to do this. This would be the best solution since this is typically something handled by a server-side language.

Now, if you host your site on GitHub Pages which is more often the case than not with Jekyll, you cannot use plugins nor can you extend Jekyll’s core with custom Ruby code, so you end up hacking a solution with what’s available: Liquid and Markdown.

The main idea is to have a file containing all our routes mapped to actual URLs. Thankfully, Jekyll allows us to define custom global variables through YAML/JSON/CSV files stored in the _data folder, later accessed via site.data.<filename>. Thus we can access those URLs directly in our pages from their name.

Creating the router

The point of the router is to expose routes (a.k.a. names) mapped to URLs (one to one). It is possible to create YAML/JSON/CSV files in the _data folder of any Jekyll project, so let’s go with a YAML file named routes.yml:

You may have noted that we are of course not restricted to internal links. We can totally define routes for external URLs to avoid typing them again and again if they tend to show up regularly. Along the same lines, we stopped at 4 routes for our example, but the router could contain hundreds of routes.

Because the file is in _data, we can access its content pretty much anywhere with site.data.<filename>.<key>. So let’s say we have a page containing the following code:

As you can see, we no longer refer to URLs, but routes instead. This is not magic, it only tells Jekyll to access global variables stored at the given path (e.g. site.data.routes.faq).

Now, if the repository is no longer hosted on GitHub or the “About us” page’s URL is now /about/, not to worry! By updating the router, we make it work without having to come back to our pages to update our links.

Adding syntactic sugar

At this point, we have a functional router allowing us to change any URL without having to crawl our site to fix broken links. So you can say it is pretty cool already. However, having to type site.data.routes.faq is not very convenient. We could surely make it a bit more elegant!

Yes and no. At first, I thought of building a small route() function accepting a key name and returning the value stored at site.data.routes.<key>. The problem is we are running Jekyll in safe mode, so we cannot extend it with Ruby code. No luck.

Then I thought of a Markdown feature I never used before: link references. This is how a link is represented in Markdown:

[I am a link](http://link.url/)

You can also set the link to head to a reference — which is completely invisible by the way, defined anywhere in the page, like so:

[I am a link][id_reference]
​[id_reference]: http://link.url

Note: parentheses are replaced with brackets when using a reference rather than a URL.

This could allow you to have all your links defined in the same place (at the bottom for instance) rather than all over the document. I must say I haven’t used this feature much, but in this case it comes in handy.

The idea is to autogenerate link references from our router so we can use our routes as reference in any file. It turns out to be surprisingly easy to do this in Liquid:

By adding this for loop anywhere in the page, this instructs Jekyll to process this as Liquid code, which will then be processed as Markdown references. So for instance, and coming back to our previous example, we could do:

Now we’re talking, right? The only problem is having to include this loop in any page. At first, I thought of adding it in the layout, so it gets automatically added to any page using the relevant layout. The problem is that layouts are not processed as Markdown in Jekyll so the references actually come in visible at the bottom of the DOM. Even worse, they are not usable in our pages since they have not been processed as Markdown… Too bad.

However we can still do something to make it slightly better. We can put this loop in a Liquid partial and include the partial in every page rather than copy-pasting the loop. Say we create a routes.html partial in the _includes folder: