My initial plan for this blog was to use Jekyll. After all: it seems to be the most popular choice. But I quickly grew frustrated with the limitations of Liquid. I wanted to use Erb, which is far more powerful and familiar.

But from everything I could find: Erb was not an option. The primary explanation seems to be that it's unsafe because it allows for the arbitrary execution of Ruby, which would be unacceptable to Jekyll's two largest customers: Github and Shopify. Most threads would lead to suggesting jekyll-rendering, which seemed promising, but it hasn't been updated in 6 years and no longer works with modern versions of Jekyll.

Welp, one of the great and terrifying things about Ruby is that anything can be modified. No library code is safe from an unscrupulous developer willing to do unholy things. I decided the method Jekyll::Renderer::render_liquid, found here, was the optimal place to cut in some Erb action. The function is universally called anytime Jekyll wants to parse a string with Liquid. What's better: it's passed all the juicy local variables intended for use inside Liquid tags. We could hijack this function and process the string with Erb, passing along the same local variables.

Thusly, the plan was hatched

There are several approaches to monkey-patching an existing function. I believe the cleanest is to create a new Module, then open the existing class and insert it via the prepend operator. Since this is a shameful thing to do, I didn't think this deserved to be made into its own gem, so I simply added a file to the ./plugins directory inside my project. I threw in the recursive-open-struct gem to make things slicker. I didn't want to lose access to any other plugin or gem available for Jekyll that might use Liquid, so rather than stop Liquid processing entirely, I decided I'd do both: first Liquid, then Erb. This would be the equivalent of a file in Middleman with the extension .liquid.erb. Behold, the hack in its entirety:

require 'erb'
require 'recursive-open-struct'

module EmbeddedRuby
  def render_liquid(content, payload, info, path = nil)
    liquid = super(content, payload, info, path)

    site = RecursiveOpenStruct.new(payload.site.to_h,
                                   recurse_over_arrays: true)
    page = RecursiveOpenStruct.new(payload.page,
                                   recurse_over_arrays: true)
    layout = RecursiveOpenStruct.new(payload.layout,
                                     recurse_over_arrays: true)
    content = payload.content
    paginator = RecursiveOpenStruct.new(payload.paginator,
                                        recurse_over_arrays: true)

    return ERB.new(liquid).result(binding)
  end
end

module Jekyll
  class Renderer
    prepend EmbeddedRuby
  end
end

As an example, in the Jekyll tutorial, there's some code for how to build a nav that uses liquid like this:

<nav>
  {% for item in site.data.navigation %}
    <a href="{{ item.link }}"
       {% if page.url == item.link %}style="color: red;"{% endif %}>
      {{ item.name }}
    </a>
  {% endfor %}
</nav>

This could now be expressed in Erb instead as:

<nav>
  <% site.data.navigation.each { |item| %>
    <a href="<%= item.link %>"
       <% if page.url == item.link %>style="color: red;"<% end %>>
      <% item.name %>
    </a>
  <% } %>
</nav>

This is not, on the face of it, much prettier. In order to really make headway, we'd want to pull in Padrino to enable helpers like link_to.

This is, in general, a bad idea, because the function render_liquid is not part of any interface contract between Jekyll and its users. The function could be changed out from under us at any time, breaking our modification. A hack like this might be justified if you're already deeply entrenched into Jekyll with a well established project that needs Erb support.

Middleman ultimately proved a much better choice for this blog. But since I could tell I wasn't the only person interested in using Erb with Jekyll, I thought this was a hack worth sharing. If you've found other ways to incorporate Erb with Jekyll, hit me up! I'd be curious to know.

What do you think? I'd love to hear from you. Here is a link to the source for this article on github. Please open an issue, send me a pull-request, or just email me at jubi@jubishop.com