Asymmetrical View

Extending Jekyll

After using Jekyll to build my personal website, I was having some issues with my stylesheet not being reloaded by Firefox. Frameworks like Ruby on Rails provide an HTML helper that add a short querystring which is simply the last modification time of the file being linked to. This interacts nicely with the browse’s cache since it can cache the stylesheet and when you change it, the URL will be different (with the new mtime) and the browser will load the new stylesheet.

Rails provides stylesheet_link_tag, which you use as follows:

  <%= stylesheet_link_tag 'app', :media => "all" %>

stylesheet_link_tag then renders the following HTML:

  <link href="/stylesheets/app.css?1234672637" media="all" rel="stylesheet" type="text/css" />

Where 1234672637I is the last modification time (or mtime) of the stylesheet file. I wanted to have the same feature in Jekyll. Jekyll uses of the Liquid text templating system, and Liquid supports being extended allowing you to add your own filters, tags and tag-blocks. Tags are used for inserting content:

   <content type="html">{ { post.content } }</content>

Filters are used to transform content, an example from the atom.xml formats the date:

   <updated>{ { post.date | date_to_xmlschema } }</updated>

What I wanted to be able to write was something like:

   { % stylesheet "/css/style.css" % }

Which meant I had to make a new tag. I created the tag file lib/jekyll/tags/stylesheet_link.rb:

  module Jekyll

    class StylesheetTag < Liquid::Tag
      def initialize(tag_name, file, tokens)
        super
        @file = file
      end

      def find_stylesheet(context)
        file = @file
        file.strip!
        file.gsub! /^["']/, ""
        file.gsub! /["']$/, ""
        if ! file =~ /.[a-z]+$/
          file = "#{file}.css"
        end

        files = [File.join(context.registers[:site].source, @file),
                 # strip a leading slash
                 File.join(context.registers[:site].source, @file[1..-1]),
                 ]
        files.each {|file|
          if File.exists? file
            return file
          end
        }
        return file
      end

      def render(context)
        file = find_stylesheet(context)

        mtime = nil
        if ! File.exists?(file)
          warn "Stylesheet file: '#{@file}' not found (#{file})"
          mtime = rand 1000000000
        else
          mtime = File.mtime(file).to_i
        end

        return %Q{<link rel="stylesheet" href="#{@file}?#{mtime}" type="text/css" media="screen, projection" />}
      end
    end

  end

  Liquid::Template.register_tag('stylesheet', Jekyll::StylesheetTag)

All tags derive from Liquid::Tag and implement both initialize and render. In the constructor the StylesheetTag saves off the argument (file). In render it attempts to find the css file and then either uses a random value or the mtime of the file (if it was found), finally returning the string representing the stylesheet link.

Finally I added the module to the list of requires in lib/jekyll.rb so it is loaded when Jekyll runs:

  ...
  # internal requires
  require 'jekyll/core_ext'
  require 'jekyll/pager'
  require 'jekyll/site'
  require 'jekyll/convertible'
  require 'jekyll/layout'
  require 'jekyll/page'
  require 'jekyll/post'
  require 'jekyll/filters'
  require 'jekyll/tags/highlight'
  require 'jekyll/tags/include'
  require 'jekyll/tags/stylesheet_link'
  require 'jekyll/albino'
  ...

Once that was completed the stylesheet tag generated links with the mtime as a query parameter and my stylesheets no longer had the caching issue. I’m offering my changes back to Tom Preston-Werner, but you can fork or clone my repository if you want to try them yourself.

Kyle Burton, 17 May 2009 – Wayne PA

Tags: jekyll,ruby,liquid