Compass: Versioning CSS files
What to expect
This post is for you if
- you are using Compass to build CSS assets and
- you want to have
screen-s1ad43fe9a2.cssinstead ofscreen.cssand - 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.htmlVersioning 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]
endWhenever 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.htmlProbably 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_busterinstead ofon_stylesheet_saved - Make storage location
current-stylesheet.htmla variable - Make pattern to search for and save within
current-stylesheet.htmlvariable
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.