Auto-versioning JavaScript & CSS files

As JavaScript becomes a more integral part to the core functionality of websites, ensuring that a user has a fresh copy of the latest code is as important as ever. Normally a browser will cache certain filetypes (CSS, JS, JPG, etc…) for an indefinite period of time. If the code within one of these cached files is updated, sometimes a browser will not know to reload that file. So, if those changes are important for the functionality of your website, your users may run into issues and not be able to use your site properly. Worse, they likely won’t have any idea what the issue is and will eventually give up and go elsewhere. But have no fear, there’s a solution to this problem… versioning your static files.

By “versioning” these files, I’m referring to changing their filenames from “my_styles.css” to “my_styles.version274.css” for example. The problem with that example is it relies on you renaming the file and updating your includes every time you make a change. That sounds like an nightmare, right? Luckily there’s a neat tactic you can you to make it appear to the browser to be a new file, but it will actually be the same file and will require no effort on your part. This variation of the versioning tactic is often referred to as “Auto-versioning”. Here’s an example…

First, we use the following rewrite rule in .htaccess:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-s # Make the file doesn't actually exist
RewriteRule ^(.*)\.[\d]+\.(css|js)$ $1.$2 [L] # Strip out the version number

Now, we write the following PHP fuction:

 *  Given a file, i.e. /css/base.css, replaces it with a string containing the
 *  file's mtime, i.e. /css/base.1221534296.css.
 *  @param $file  The file to be loaded.  Must be an absolute path (i.e.
 *                starting with slash).
function auto_version($file)
  if(strpos($file, '/') !== 0 || !file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
    return $file;
  $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
  return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);

Now, wherever you include your CSS, change it from this:

<link rel="stylesheet" href="/css/base.css" type="text/css" />

To this:

<link rel="stylesheet" href="<?=auto_version('/css/base.css')?>" type="text/css" />

And when rendered, will appear as:

<link rel="stylesheet" href="/css/base.1251992914.css" type="text/css" />

How does this work? Because that mod_rewrite rule above will strip out the timestamp, your server will deliver /css/base.css as the requested file. But to the user’s browser, it will appear as a different file whenever it is updated.


Note: Some strategies to fix this caching issue is to append a version # onto the end of the css or JavaScript file with a querystring, such as:

<link rel="stylesheet" href="/css/base.css?version=1234" type="text/css" />

This method will actually prevent the file from being cached, period! This is bad. You want the benefits of caching. You don’t want to make your users pull down 100k of JavaScript & CSS files on every page load. More importantly, your servers could start to strain under the additional load.

Why is it not cached? See Section 13 from the HTTP Specification, in particular Section 13.9

13.9 Side Effects of GET and HEAD

Unless the origin server explicitly prohibits the caching of their responses, the application of GET and HEAD methods to any resources SHOULD NOT have side effects that would lead to erroneous behavior if these responses are taken from a cache. They MAY still have side effects, but a cache is not required to consider such side effects in its caching decisions. Caches are always expected to observe an origin server’s explicit restrictions on caching.

We 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. This specifically means that responses from HTTP/1.0 servers for such URIs SHOULD NOT be taken from a cache. See section 9.1.1 for related information.

  • jj

    Good article, this is the preferred method for sure.
    However, you should be using this on any static file- images, pdfs, etc.

    Also, would be nice to see how you’re updating image references ‘within’ a css file. I still don’t have a good methodology for this.

  • Derek

    @JJ, There are two ways that I do this. I usually prefer (A).

    A) I will actually move the dynamic styles out of the CSS file and into your main site template (ex. index.php). Then, for example:

    #some_node{background:url(‘<?php auto_version(“/images/foobar.jpg”); ?>’);)

    I use that method for JavaScript variables that are generated from PHP. That way I can still have the majority of my JavaScript in its own .js files.

    ==Another method==

    B) You can actually run any type of file through the PHP parser, it doesn’t have to end in .php. Also, you can use any type of file as a CSS file, it doesn’t have to end in .css. This gives you the ability to either put all your CSS code inside a .php file, but make sure you have it throw the CSS mime type header at the top. If you wanted to have the PHP parser actually parse your .css files, you can add the following line to your .htaccess file.

    AddType application/x-httpd-php .css

  • Dave Styles

    Not sure why but I had to change \d to [0-9] in the ReWrite rule to get this working. Otherwise very, very helpful and clear.

  • Rob

    It’s a pitty but the rewrite rule doesn’t work for me (either [/d] nor [0-9]). It doesn’t strip out the timestamp :( Link to the test page:

    Could someone please help me, because the method posted by Derek is excellent!

  • Rob

    i don’t know really how but is has started to work finally :)

  • Derek

    Thanks Rob. Glad to hear it worked for you.

  • Rob

    Ok, at last i’ve found the reason: i’ve been modifying some .htaccess file with MAC format, than with notepad++ i changed it to DOS/WINDOWS and voile :) Thanks Derek!

  • Leola Lloyd

    It’s a pitty but the rewrite rule doesn’t work for me (either [/d] nor [0-9]). It doesn’t strip out the timestamp :( Link to the test page: Could someone please help me, because the method posted by Derek is excellent!

  • Yvette Ball

    i don’t know really how but is has started to work finally :)

  • Thompson

    This is REALLY cool. I got it working on the first try.

    Is it possible to modify this to work with a fully qualified url?
    (e.g. I use a cookieless subdomain to serve static content like JS, so I have to specify the full url for that.

  • Thompson

    I came up with a really simple solution to be able to use a fully qualified url. I just put the function around the url that is relative to the root, and then wrote the rest of my fully qualified url in front of it like so:

    '' . auto_version('/site/themes/mytheme/js/global.js')

    So obvious now. Yet so effective. :) And now I can even serve it off a CDN if I want!

  • Ryan Sharp

    Wow, you really are quite clueless.

  • cold call scripts

    For most recent news you have to pay a visit world-wide-web and on web I found this site as a best web
    page for latest updates.

  • Fjr advisors complaints

    I’m not sure exactly why but this blog is loading extremely slow for me. Is anyone else having this problem or is it a issue on my end? I’ll check back later and see if
    the problem still exists.

  • Daivd Huosãr

    Just thought I’d let anyone know who stumbles across this as I have…

    I had to write my CSS url like this:

    <link rel="stylesheet" href="” type=”text/css” />

    Thanks Derek for the great script!

  • Daivd Huosãr

    hmmm code editor fail… lets try that again.

    <link rel="stylesheet" href="" type="text/css" />

  • Daivd Huosãr

    Nope – it’s wiping my php.

    That href should read:

    ?php echo auto_version(‘/base.css’); ?

    + obviously add in the on either end.

  • Daivd Huosãr

    Oh ffs..

    * + obviously add in the TRIANGLE brackets on either end.

  • Reni Margold

    thank you, this is really a very elegant technique!

  • Alexander

    God bless you, sir.

  • Denise

    I’ve been looking for this a long time! Thanks for sharing it!
    I need some help here.!
    I tried to follow your instructions, changed the htaccess file, inserted on the PHP funtion on one of my pages and so on..
    But once I upload the modified page to the server and try to see it, it get this error: “internal server error”.
    I can’t figure out what I did wrong.

  • Akinmade Bond

    Thank you for this. :)