1. What to expect
  2. The setup
  3. Versioning assets with Compass
    1. Step 1: Versioning CSS asset
    2. Step 2: Inserting versioned stylesheet into HTML <head>
  4. Room for improvement

What to expect

This post is for you if

  • you are using Compass to build CSS assets and
  • you want to have screen-s1ad43fe9a2.css instead of screen.css and
  • you want the hash to change only when styles change
  • you want to automatically update any <link> reference to your new stylesheet name.

It contains a step-by-step guide to achieve this setting with Compass.

Versioning your CSS files in this way is particularly useful to

  • allow long asset cache lifetimes while allowing you to push current stylesheets to your users (covered here)
  • allow full page caching.

The setup

For this tutorial, I’ll assume a sample project with this structure

project/
    assets/
        css/
        scss/
    config.rb
    index.html

Versioning assets with Compass

Sprites are automatically versioned by Compass in the scheme my-sprite-s{HASH}.png.

For stylesheets, we follow the same naming pattern and will come up with screen-s{HASH}.css in the end.

There is an official hook in Compass’ configuration options, asset_cache_buster. Although it is well documented, I couldn’t get it to work properly on my machine.

Therefore, I took a different path by hooking into on_stylesheet_saved (docs). Whenever a stylesheet is saved, its contents are hashed, this hash is inserted into the filename, and our stylesheet saved with the new filename.

In an additional step, the new reference is then published into the <head> area of our index.html, so no tedious manual updating is required.

Step 1: Versioning CSS asset

For adding a hash representing current file contents to your filename, insert the below snippet at the end of your Compass config.rb file.

# config.rb

# ...

# Cache buster
on_stylesheet_saved do |filename|
    if File.exists?(filename)
        target = target_filename filename
        FileUtils.mv filename, target
    end
end

def digest_file filename
    contents = File.read filename
    digest contents
end

def target_filename filename
    result = ""
    result += filename[/(.+).css/, 1]
    result += "-s"
    result += digest_file filename
    result += ".css"
end

def digest string
    require "digest/md5"
    result = Digest::MD5.hexdigest(string)
    result[0, 10]
end

Whenever you change your SCSS and run compass compile, it should now create a new CSS file with a different hash.

Warning: If you run compass watch, it will create a whole lot of files when you actively develop your stylesheets. Usage of git to get rid of all superfluous files with ease is recommended!

I’ll wait here while you get that up and running on your project.

Oh, great, you are back! Let’s move on.

Step 2: Inserting versioned stylesheet into HTML <head>

Versioning CSS filenames requires to change the references to it as often as you change your styles. Nobody wants to do that by hand, so here’s some automation for us.

Therefore you’ll have to put some more additions into config.rb.

Moreover, instead of patching index.html directly, I added an intermediate file named current-stylesheet.html to have all those automated changes on one file only, keeping the git history of index.html relevant.

It is kept here:

project/
    assets/
        css/
            current-stylesheet.html

Probably that’s not optimal for you, so tinker around to change it.

I’m then including this current-stylesheet.html in index.html - which is PHP, actually.

# index.html
<html>
    <head>
        <?php echo file_get_contents(__DIR__.'/assets/css/current-stylesheet.html'); ?>
        ...
    </head>

If you are not familiar with PHP, this line just prints the contents of current-stylesheet.html at the given location.

The initial contents of current-stylesheet.html should be:

# current-stylesheet.html
<link rel="stylesheet" media="screen" type="text/css" href="/assets/css/this-will-be-replaced-anyway.css" />

Important part: /assets/css/ should be contained in path, else the below script will not recognize the link reference.

The following changes are required in config.rb. Note that this is NOT a complete version, but rather highlighting changes to the above. A full version is provided below.

# config.rb

# ...

# Cache buster
on_stylesheet_saved do |filename|
    if File.exists?(filename)
        target = target_filename filename
        FileUtils.mv filename, target

        asset_path = asset_path target, css_dir

        puts "   rename #{asset_path}"
        set_stylesheet_path asset_path, css_dir
    end
end

def set_stylesheet_path path, css_dir
    target_file = "#{css_dir}/current-stylesheet.html"

    contents = File.read target_file

    node_regexp = Regexp.new "<link .*#{css_dir}.*>"
    original_node = contents[node_regexp]
    original_href = original_node[/href="(.*)"/, 1]

    contents[original_href] = "/#{path}"

    File.write target_file, contents

    puts "   update #{target_file}"
end

def asset_path filename, css_dir
    regexp = Regexp.new "#{css_dir}.*"
    filename[regexp]
end

# rest of the above
# ...

Finally when compiling your CSS, the file will be saved with a versioning hash. Then, the new file path will be <link>ed in current-stylesheet.html, which is automatically included in your document <head>.

Great, right?

Room for improvement

The above is more of a “proof of concept”. There is plenty room for improvement:

  • Use asset_cache_buster instead of on_stylesheet_saved
  • Make storage location current-stylesheet.html a variable
  • Make pattern to search for and save within current-stylesheet.html variable

And once you have CSS versioning in place, you will notice that you absolutely need this for JavaScript assets as well. But that’s a different story.