Automatic asset versioning in Django | Comments (9)
Posted in Code, performance on 8th April 2008, 1:45 am by Stuart
Following on from Ed’s “Automatic versioning of CSS, JavaScript and Images” here is a method to version filenames based on modification times to be used in Django as a template tag.
This is really handy technique for when you set expires headers to a long way into the future. With headers set in this way files need to have their filenames versioned to force the client to download the latest version of a file. The purpose being that with far futures expires the browser will agressively cache the asset thus minimising the amount of requests for assets.
This template tag for Django uses the same method as Ed’s to provide a version string that’s appended to the filename. The awesome part of this is once configured you can just happily change the file as you need to and the caching is taken care of.
{% load utils %}
<link rel="stylesheet" type="text/css" href="{% version '/static/css/style.css' %}">
The code for this is placed in a utils.py in “project/app/templatetags”
from django import template
register = template.Library()
import os, re
STATIC_PATH="/path/to/templates/"
version_cache = {}
rx = re.compile(r"^(.*)\.(.*?)$")
def version(path_string):
try:
if path_string in version_cache:
mtime = version_cache[path_string]
else:
mtime = os.path.getmtime('%s%s' % (STATIC_PATH, path_string,))
version_cache[path_string] = mtime
return rx.sub(r"\1.%d.\2" % mtime, path_string)
except:
return path_string
register.simple_tag(version)
As Brad has pointed out in the comments below the original query string method used will not be cached by UAs following the http spec. The output will now look like the following:
/static/css/style.css?v=1207433992
/static/css/style.1207433992.css
In addition a mod_rewrite rule is required to map the versioned file to the original.
RewriteRule ^/static/(.*?)\.[0-9]+\.(css|js|jpe?g|gif|png) /static/$1\.$2 [L]

Hey Stu,
It seems Opera and Safari don’t cache file paths with query strings, as per the HTTP spec, and given that it’s part of the spec we have no guarantee that other browsers won’t behave the same in the future. Probably best to do something like:
return path_string.replace('.css', '.%d.css' % mtime)And have something like this in mod_rewrite:
RewriteRule ^/static/([a-z-]+)\.[0-9]+\.(css|js) /static/$1\.$s [L]This keeps the filenames looking like:
/static/css/style.1207433992.css, but rewrites them to the latest versions in your static server.Oops, my mod_rewrite had fail:
RewriteRule ^/static/([a-z-]+)\.[0-9]+\.(css|js) /static/$1\.$2 [L]Is better.
@Brad: Excellent points well made. I’ve updated the script accordingly.
I have a feeling that this will provide very very useful to a number of people :>
I was just looking around for some background to write something just like this for Django!
Thanks very much for saving me several hours!
- rich
Great stuff, thanks a lot
One note thought:
Atually as Steve pointed out in his comment to High Performance Web Sites: Rule 3 – Add an Expires Header the HTTP 1.1 specs says that URLs with query strings should not be cached only when there is no explicit expiration information:
From HTTP 1.1 spec, section 13.9:
e note one exception to this rule: since some applications have
traditionally used GETs and HEADs with query URLs (those containing a
“?” in the rel_path part) to perform operations with significant side
effects, caches MUST NOT treat responses to such URIs as fresh unless
the server provides an explicit expiration time.
Also, RoR have some kind of template tag for auto versioning and it uses QS as far as I know.
– Lukasz
Recently I found this very interesting post, by Steve, about proxy and query strings issues. Steve proved that Squid’s default configuration is not to cache URLs with query strings.
http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/
So at the end it seems that using conservative file name versioning is better and more safe than QS method.
For the regexp substitution, I think you mean:
rx.sub(r"\1.%d.\2" % mtime, path_string)Without the raw string you’ll get binary in your return value the dots will be wildcards.
This was a helpful article, though. Thanks!
@Ben: Thanks – I’ve updated the code example