Automatic Versioning Of JS And CSS Files In WordPress

If you’re a WordPress developer, this will probably sound familiar: you make a change to  one of your scripts or style sheets, reload the page you’re working on to see the result, and… everything stays the same.

Of course, after a moment of confusion, you realise that you didn’t update the $version argument in your wp_register_script() or wp_register_style() call (you are using the dependency APIs and not outputting the <script> or <style> tags directly, right?), so the browser is still using its old, cached version of the file. So you either force-refresh the page, or go back to the source code and change the version.

This can become annoying very quickly. Worse yet, if you forget to properly version your dependencies when you ship the code to users, you can get a number of seemingly bogus bug reports caused by their browsers still using the cached scripts and styles instead of the new files.

Automate It

One common solution is to append the file modification time to the URL. This will cause the browser to automatically reload the file when it changes. Here’s how you can to do that with the WP dependency API:

wp_enqueue_script(
	'my-script-handle',
	plugins_url('js/my-script.js', __FILE__),
	array('dependency1', 'dependency2'),
	filemtime(dirname(__FILE__) . '/js/my-script.js')
);
// The resulting URL will look something like this:
// http://.../wp-content/plugins/my-plugin/js/my-script.js?ver=1234567890

For themes, the code would be very similar, except you would use get_stylesheet_directory_uri() and get_stylesheet_directory() to get the script URL and file name, respectively.

A Better Way

This approach does have a drawback: according to Google, most proxies don’t cache URLs with a query string:

Most proxies, most notably Squid up through version 3.0, do not cache resources with a “?” in their URL even if a Cache-control: public header is present in the response. To enable proxy caching for these resources, remove query strings from references to static resources, and instead encode the parameters into the file names themselves.

So if you want to minimize your page load time, add the modification time to the file name instead of the query string. To make that work, you will need to do two things.

First, change code that registers or queues your scripts and style sheets to something like this:

$mtime = filemtime(dirname(__FILE__) . '/js/my-script.js');
wp_enqueue_script(
	'my-script-handle',
	plugins_url('js/my-script.' . $mtime . '.js', __FILE__),
	array('dependency1', 'dependency2'),
	null
);
// The resulting URL will look something like this:
// http://.../wp-content/plugins/my-plugin/js/my-script.1234567890.js

Passing NULL as the $version argument disables the default ?ver=WordPress-version query string that WordPress automatically adds to all dependencies that don’t explicitly specify a version number.

Then create a .htaccess file in your plugin or theme directory and place the following code in it:

# Auto-versioning support
<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L]
</IfModule>

This ensures that the web server will output the right file when the browser requests an auto-versioned URL. For example, asking for script.1234567890.js will load script.js.

If you control the site in question, you can add the above rewrite rules to the main WordPress .htaccess file instead of a plugin- or theme-specific one.

A word of warning:  Some servers don’t have mod_rewrite installed. If you’re using this code in a plugin or theme, it either mention mod_rewrite as a requirement, or stick to the first, query string based approach.

Auto-Versioning Library

I’ve written a small PHP library that further simplifies doing automatic CSS/JS versioning in WordPress. It adds auto-versioning wrappers for the wp_register_* and wp_enqueue_* functions, as well as a way to automatically version all CSS and JS files with just two lines of code. Finally, it lets you choose between the query string approach and the timestamp-in-filename approach just by setting a flag.

You can download it here:

auto-versioning.zip

Usage

To auto-version your files:

Simply replace all wp_register_* and wp_enqueue_* calls with wp_register_auto_versioned_* and wp_enqueue_auto_versioned_*, and remove the $version argument if you were using it. Here’s an example:

require_once 'auto-versioning.php';
wp_enqueue_auto_versioned_style(
	'my-style-handle',
	plugins_url('css/style.css', __FILE__),
	array('dependency1', 'dependency2'),
	'all',
	false //Optional. true = timestamp in filename, false = in query string (default).
);
//The  tag that WP outputs will have a href similar to this:
//http://.../wp-content/plugins/my-plugin/css/style.css?ver=1234567890

The library will automatically figure out the filesystem location of the style sheet based on its URL, get the modification time, and pass it to wp_enqueue_style() as a version number so that it is added to the query string.

If you want to place the timestamp in the file name instead, simply pass True as the last argument (after $media for styles or $in_footer for scripts). You will still need to add the necessary rewrite rules to the .htaccess file – the library doesn’t handle that for you.

To auto-version all files:

require_once 'auto-versioning.php';
AutoVersioning::apply_to_all_dependencies(false);

This will apply automatic versioning to all files enqueued  using WordPress dependency functions. This can be handy if you’re the owner of the site and want to auto-version everything in one go. You can just drop auto-versioning.php in your theme directory and add the above code to your functions.php.

The only parameter of the apply_to_all_dependencies() method specifies whether to add the file modification time to the file name (true) or the query string (false; this is the default). Again, don’t forget to add the requisite code to your .htaccess if you set this to true.

Hint

You can able to detect if mod_rewrite is installed by checking if WordPress is using mod_rewrite permalinks. This way you can automatically switch between putting the modification time in the query string  and the file name.  Here’s an example:

global $wp_rewrite;
wp_enqueue_auto_versioned_style(
	'my-style-handle',
	plugins_url('css/style.css', __FILE__),
	array('dependency1', 'dependency2'),
	'all',
	$wp_rewrite->using_mod_rewrite_permalinks()
);
Related posts :

7 Responses to “Automatic Versioning Of JS And CSS Files In WordPress”

  1. Your technique works great! I haven’t tried your library, yet. The equivalent Nginx rewrite rule goes like this…

    rewrite “^(.*)\.[\d]{10}\.(css|js)$” $1.$2 last;

    Hope this helps someone too.

    Thank you the idea.

    PS: I’d love to get your blog posts by email. Currently, subscribed via RSS. Thanks.

  2. Jānis Elsts says:

    Actually, email subscriptions are enabled for my feed, it’s just that this fact isn’t publicized very well. You can subscribe by email here.

  3. Thank you. I could subscribe now via Feedburner. Thanks again for the help.

  4. Ben May says:

    Thanks for this – VERY helpful!

  5. Zach says:

    Hi,
    I’m rewriting theme and plugin URLs like so:

    function roots_add_rewrites($content) {
    global $wp_rewrite;
    $roots_new_non_wp_rules = array(
    ‘css/(.*)’ => THEME_PATH . ‘/css/$1′,
    ‘js/(.*)’ => THEME_PATH . ‘/js/$1′,
    ‘img/(.*)’ => THEME_PATH . ‘/img/$1′,
    ‘plugins/(.*)’ => RELATIVE_PLUGIN_PATH . ‘/$1′
    );
    $wp_rewrite->non_wp_rules = array_merge($wp_rewrite->non_wp_rules, $roots_new_non_wp_rules);
    return $content;
    }

    function roots_clean_urls($content) {
    if (strpos($content, FULL_RELATIVE_PLUGIN_PATH) === 0) {
    return str_replace(FULL_RELATIVE_PLUGIN_PATH, WP_BASE . ‘/plugins’, $content);
    } else {
    return str_replace(‘/’ . THEME_PATH, ”, $content);
    }
    }

    // only use clean urls if the theme isn’t a child or an MU (Network) install
    if (!is_multisite() && !is_child_theme() && get_option(‘permalink_structure’)) {
    add_action(‘generate_rewrite_rules’, ‘roots_add_rewrites’);
    add_action(‘generate_rewrite_rules’, ‘roots_add_h5bp_htaccess’);
    if (!is_admin()) {
    $tags = array(
    ‘plugins_url’,
    ‘bloginfo’,
    ‘stylesheet_directory_uri’,
    ‘template_directory_uri’,
    ‘script_loader_src’,
    ‘style_loader_src’
    );

    add_filters($tags, ‘roots_clean_urls’);
    }
    }

    which turns these asset URLs into mysite.com/js/scripts.js (for themes) and mysite.com/plugins/pluginname/js/script.js (for plugins), but when running the script you mentioned, it is saying there is an empty delimiter on line 70 (referencing foreach($url_mappings as $root_url => $directory) — but more specifically, the $directory parameter). Any thoughts? Thanks!

  6. Jānis Elsts says:

    It looks like your roots_clean_urls callback makes get_stylesheet_directory_uri() return an empty string, which in turn crashes the script when it attempts to check if the script/CSS URL contains the stylesheet directory. I’m not sure what a good solution would be in this situation.

  7. [...] Short story: I am using this technique to auto-version my css and js files by adding a string to the filename with filemtime(): http://w-shadow.com/blog/2012/07/30/automatic-versioning-of-css-js/ [...]

Leave a Reply