cognitive coding

Concerning Slim and Liquid

It’s probably not a good thing to start a blog with a rant, but in the making of this blog there were some pitfalls, that caused me some hours of bug hunting and headache. So, what’s better to start a blog with than a meta post about this blog.

So, imagine you have a Jekyll blog, just like this one. And you want to use Slim instead of ERB—because ERB syntax sucks—, just like this one. Since you use Jekyll, you also use Liquid, and you mix your Liquid tags and your Slim templates and everything is fine. Until…

One day, you decide to have the home page show the latest blog post, so you add some code like this:

{% assign page = site.posts.first %}
{% assign content = page.content %}
{% include post_content.slim %}

and your post_content.slim partial has some line somewhere, that looks somewhat like this:

{{ content }}

You check your index page and everything seems to be fine; but then you take a closer look. Some words seem to be missing. After some time and with the help of the “Inspect element” tool of your favourite browser, you figure out that the words are not gone, but converted to HTML tags.

Another oddity you recognize: When you visit the post itself (as in: not on the index page), everything seems to work correctly. But since you already spent some hours building the whole blog, you decide to write a little blog post instead of fixing this now. You tell people about some cool Ruby project you recently worked on, and insert some fancy code snippet that shows exactly how awesome your project is. Maybe something like this:

def print_hi(name)
  puts "Hi, #{name}"
end

Unfortunately, now your blog won’t build! The cryptic error message tells you it’s something pattern related.

Okay, back to the first error, since that compiled at least. You start to wonder, if the tags were created because some text was injected into the Slim markup in some wrong way, so that on each line, the first word will be translated into a tag. And indeed, when you remove all newlines via a Liquid filter, the lost words reapper. That’s more of a hack than a solution, but at least it works.

So, back to problem number two. After some time, you take a closer look at what seemed to be an unrelated error message. Parts of HTML code are shown. Especially one part sticks out:

<span class="si">#{</span>

This code was generated by the syntax highlighter. Unfortunately, inserting this code into a Slim file will cause the compiler to break. It tries to interpret the #{ as the beginning of a Ruby string interpolation and subsequently considers </span> to be Ruby code. / in Ruby is the delimiter for regular expressions. Mystery solved.

Not quite. You still have to fix this. Your first solution would be to replace all occurences of #{ with the escaped version \#{ with a filter like

{{ content | replace:'#{','\#{' }}

And you fail again. Liquid also interprets the beginning of a string interpolation and complains. So you write a custom filter that does exactly what you want. Now everything kind of works, but is ugly as hell.

Your final deed of the day is to throw everything you “fixed” away and write a generator that duplicates the latest post and saves it as the index page:

module Jekyll
  class IndexDraft < Draft
    def destination(dest)
      File.join(site.dest, "index.html")
    end
  end

  class IndexPost < Post
    def destination(dest)
      File.join(site.dest, "index.html")
    end
  end

  class IndexPageGenerator < Generator
    def generate(site)
      last_post = site.posts.last

      src_path = last_post.path
      src_filename = File.basename(src_path)
      src_dirname = File.dirname(src_path)

      last_post.define_singleton_method(:next) {}

      if src_dirname.end_with? "_drafts"
        new_post = IndexDraft.new(site, site.source, File.dirname(src_dirname), src_filename)
      else
        new_post = IndexPost.new(site, site.source, File.dirname(src_dirname), src_filename)
      end

      new_post.define_singleton_method(:previous) { last_post.previous }
      site.posts << new_post
    end
  end
end

And you finally have your index page with the latest post. Easy, huh?

Oh and in case you also use the jekyll-tagging plugin, make sure to use my forked version over at GitHub to avoid having duplicate entries on tag pages. You can include it in your Gemfile like this:

gem 'jekyll-tagging', git: 'https://github.com/phyrog/jekyll-tagging'

Update May 16, 2016: I have since relaunched my blog, which is now based on Jekyll 3. Jekyll 3 features a new Hook API, that allows me to do this and other things a lot more elegant.

Jekyll::Hooks.register :site, :post_write do |site|
  File.open(File.join(site.dest, 'index.html'), "w") do |f|
    f.write site.posts.last.to_s
  end
end