Robot Has No Heart

Xavier Shay blogs here

A robot that does not have a heart

Nanoc3 with Rack::StaticCache

There is a neat piece of middleware introduced in rack-contrib 0.9.3 called Rack::StaticCache. It allows you to version your static assets (images, css) so that you can set infinite expires headers on them. All you need is a version number trailing your file name, and it is routed through to the underlying file. Whenever you change the file, you change the version.

1
2
/img/lolcat-1.jpg -> /img/lolcat.jpg
/img/lolcat-2.jpg -> /img/lolcat.jpg

The URLs go to the same place, but since they are different you can cache them indefinitely and change all the referencing URLs in your code when you change the asset. That’s annoying if you’re trying to do it by hand, but that’s why we have code eh. I wrote a nanoc3 after filter that parses the HTML using nokogiri, and replaces any reference to any image or stylesheet with a reference versioned using the last modified timestamp of that asset. It automatically updates! This is particularly neat because you can link in images in markdown without ever worrying about versioning.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# lib/static_cache_filter.rb
require 'nokogiri'

class StaticCacheFilter < Nanoc3::Filter
  identifier :static_cache

  def run(content, params = {})
    doc = Nokogiri::HTML::Document.parse(content)
    add_version = lambda {|attr| lambda {|x|
      src = x[attr]
      item = @items.detect {|y| y.identifier == "#{src.gsub(/\..+$/, '')}/" }
      if item
        version = item.mtime.to_i
        tokens = src.split('.')
        src = tokens[0] + "-#{version}." + tokens[1..-1].join('.')
        x[attr] = src
      end
    }}
    doc.css('img'                 ).each(&add_version['src'])
    doc.css('link[rel=stylesheet]').each(&add_version['href'])
    doc.to_html
  end
end
1
2
3
4
5
6
# Rules
compile '/' do
  filter :haml
  layout 'home'
  filter :static_cache
end
1
2
3
# config.ru
use Rack::StaticCache, :urls => ['/img','/css'], :root => "public"
run Rack::Directory.new("public")

Nanoc3 and CoffeeScript

Nanoc3 is a pretty awesome static site generator. It works by running your content through “filters” to create the final static site. It comes with a lot of built in filters – Haml, Sass, rubypants, markdown, and more! Nothing for Javascript though. Which is sad because I really like CoffeeScript. It’s ok! I wrote my own filter, shared here for your enjoyment.

Bang this in your lib folder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
require 'open3'
require 'win32/open3' if RUBY_PLATFORM.match /win32/

class CoffeeFilter < Nanoc3::Filter
  identifier :coffee

  def run(content, params = {})
    output = ''
    error = ''
    command = 'coffee -s -p -l'
    Open3.popen3(command) do |stdin, stdout, stderr|
      stdin.puts content
      stdin.close
      output = stdout.read.strip
      error = stderr.read.strip
      [stdout, stderr].each { |io| io.close }
    end

    if error.length > 0
      raise("Compilation error:\n#{error}")
    else
      output
    end
  end
end

To use it, a compilation rule like the following is pretty neat:

1
2
3
4
5
6
7
8
9
# Compile both coffee and js, co-mingled in the same directory
compile '/js/*' do
  case item[:extension]
    when 'coffee'
      filter :coffee
    when 'js'
      # Nothing
  end
end

Don’t forget to add ‘coffee’ to the list of text extensions in your config.yaml!

Protip: You can use the above pattern to filter content through any command line program. Figlet anyone?

A pretty flower Another pretty flower