Andrew Welch
Published , updated · 5 min read · RSS Feed
Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.
Simple Static Asset Versioning in Craft CMS
Static asset versioning allows you to have a performant website, while also ensuring the latest CSS and JavaScript are delivered
Static assets are things like CSS, JavaScript, images, etc. that don’t require being dynamically rendered. Because we want to leverage browser caching of static resources, we set the expires header for these resource to be 30 days or more.
This tells the client browser that it can cache these resources locally, so that the next time the page is visited, it doesn’t have to go out over the wire to download them. It already has a cached copy!
Any time we can avoid downloading things altogether, it’s a big gain — especially on high latency mobile devices with limited (and often metered) data bandwidth.
In an ideal world, these static resources are also delivered by a server geographically close to the client browser, via a Content Delivery Network (CDN). Check out the A Pretty Website Isn’t Enough article for more performance-related discussion.
This is all great, but what happens when we change the CSS or JavaScript?
When we make changes to the CSS or JavaScript, we need some way to tell the client browser “Hey, I know you have this thing cached, which is great, but there’s a new version of it, and we really would like you to download it.”
This is where static asset versioning comes in. We want some way to indicate the version of the thing that’s being requested, so that if the client browser doesn’t have the latest version, it can download it. In the past, the query string was used for this, and you’d have something like:
/css/site-css.min.css?v=325329
This is known as query string based cache busting, and while it can work, it doesn’t play that nice with caching, proxies and CDNs.
So instead, we can use something called filename based cache busting, which would look something like this:
/css/site-css.min.325329.css
Since the version is part of the filename, a change in the version looks to the browser like a request for an entirely different file.
Link Keep It Simple, Stupid
So great, how do we do this? As with anything frontend dev related, there are many ways to accomplish it. You can use something like gulp-rev to generate a manifest file for every static resource, and use that for your versioning scheme.
However, I try to keep the toolchain and build system as simple as possible, because I believe in Murphy’s Law:
Anything that can go wrong, will go wrong.
So unless there is a really compelling reason for a hellaciously complicated system, I try to avoid it. As with anything, there are downsides to my approach, and I don’t use it for every project. But here’s one way to do it.
A number of people read the A Better package.json for the Frontend & Implementing Critical CSS on your website articles noticed a funny little thing in the code: {{ staticAssetsVersion }}
That’s just my simple filename-based cache busting system.
Link Server Setup
The first thing we need to do is add a simple rule to our server. Remember, the site-css.min.css file is the same file, we’re just using the version string as a way to bust the cache.
So wouldn’t it be great if we could tell our server to completely ignore the version string, and just serve up the actual file? As it turns out, we can do this pretty easily.
So any requests that come in for site-css.min.XXXX.css (where the XXXX is a version number) will cause the server to ignore the version number, and simply serve up the file site-css.min.css.
For Apache, we do this:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*?\/)*?([a-z\.\-]+)(\d+)\.(bmp|css|cur|gif|ico|jpe?g|js|png|svgz?|webp|webmanifest)$ $1$2$4 [L]
</IfModule>
And for Nginx we do this:
location ~* (.+)\.(?:\d+)\.(js|css|png|jpg|jpeg|gif|webp)$ {
try_files $uri $1.$2;
}
That’s it, just restart your webserver, and like magic, any numbers before the static asset filename extension are removed. I’ve built this into the Nginx-Craft boilerplate, if you’d like to give that a go.
The nice thing about this is that you then don’t need to exclude your static assets from your git repo, because you don’t have hundreds of named files clogging it up. Each file has one name, it’s just the request URL that changes based on the version (which is stripped out by the server).
This allows you to eliminate an intermediate build step on deploy, so a simple git pull will result in a completely functional website.
If you’re using Laravel Valet for local dev, I’ve written a LocalValetDriver you can just drop in your project root, and everything will “just work” from a server-config point of view.
Link Frontend Setup
So then now how do we actually change this version number on the frontend, to make sure that it the browser cache gets busted when we change our static resources?
So in Craft CMS, we just add an environment variable to our general.php:
return array(
// All environments
'*' => array(
'omitScriptNameInUrls' => true,
'usePathInfo' => true,
// Set the environmental variables
'environmentVariables' => array(
'staticAssetsVersion' => '84',
),
),
// Live (production) environment
'live' => array(
'devMode' => false,
'enableTemplateCaching' => true,
'allowAutoUpdates' => false,
),
// Staging (pre-production) environment
'staging' => array(
'devMode' => false,
'enableTemplateCaching' => true,
'allowAutoUpdates' => false,
// Set the environmental variables
'environmentVariables' => array(
'staticAssetsVersion' => time(),
),
),
// Local (development) environment
'local' => array(
'devMode' => true,
'enableTemplateCaching' => false,
'allowAutoUpdates' => true,
// Set the environmental variables
'environmentVariables' => array(
'staticAssetsVersion' => time(),
),
),
);
So we have a single environmental variable called staticAssetsVersion that’s set to a number (84 in this case) in all environments. When we make changes that we deploy to production, we can change this number, push it, and away we go.
At first glance, this seems insane: manually changing anything in this day and age seems incredibly backwards.
However, the reality is that actual pushes to production are generally quite infrequent.
We can also take advantage of Gulp to automate this away:
// static assets version task
gulp.task("static-assets-version", () => {
gulp.src(pkg.paths.craftConfig + "general.php")
.pipe($.replace(/'staticAssetsVersion' => (\d+),/g, function(match, p1, offset, string) {
p1++;
$.fancyLog("-> Changed staticAssetsVersion to " + p1);
return "'staticAssetsVersion' => " + p1 + ",";
}))
.pipe(gulp.dest(pkg.paths.craftConfig));
});
The vast majority of the changes are made in local development, and on staging for the client to approve the changes after they’ve been tested.
In these environments, we set 'staticAssetsVersion' => time() so that the cache is always busted.
Then at the very top of our main layout.twig we just do:
{% set staticAssetsVersion = craft.config.environmentVariables.staticAssetsVersion %}
And then we can simply do this to request our static assets:
<link rel="stylesheet" href="{{ baseUrl }}css/site-css.min.{{staticAssetsVersion}}.css">
Boom. That’s it. Now when we’ve made changes, the client has approved them, and we’re ready to deploy to production, we just bump the number in our general.php file, and deploy the changes to live production.
Link So What Are the Downsides?
One common complaint about this relatively simple approach to static asset versioning is that since we have the built static assets such as our CSS in the git repo, how do we handle merge conflicts?
“Dealing with merge conflicts in a minimized CSS file is a nightmare!” they say.
I think this is mostly a red herring, because the final minimized CSS file is something that’s built from your source. If someone else committed a change, no big deal, accept their version, then re-build it, and commit yours.
You don’t ever need to figure out the diff, or resolve the merge conflicts at all.
Another downside to this system is that there’s one global version for everything. That means if you only change the CSS, too bad, the JavaScript will be cache busted site-wide as well.
Use whatever system is best for your project; build systems are not a one size fits all proposition.
So clearly, this system is not perfect. But it does fit the Keep It Simple, Stupid (KISS) methodology, and for many sites, it’s all you’re going to end up needing.
There are certainly cases where a manifest file with per-resource versioning is better, such as if you’re constantly pushing changes to live production, with a large team working together on the same static assets, and you have a large number of static assets that you’d ideally like to be individually cache busted.
But use project-appropriate tooling in your build system, rather than trying to turn everything into a hammer. You might like to be able to create a build system that you never change, and use for every single site you use, but magic 8‑ball says:
It’s just not that likely to happen with the pace of the JavaScript and webdev worlds. Obviously we want to re-use as much as we possibly can, and leverage our past work, but the reality is that your tooling should reflect the project you’re working on, and it’s going to be constantly changing and evolving.
That’s it folks, enjoy your day.