Andrew Welch · Insights · #frontend #devops #craftcms

Published , updated · 5 min read ·


For more tools, technologies, and techniques, check out the devMode.fm podcast!

Simple Static Asset Versioning in Craft CMS

Sta­t­ic asset ver­sion­ing allows you to have a per­for­mant web­site, while also ensur­ing the lat­est CSS and JavaScript are delivered

Coffee Beans Static Asset Versioning

Sta­t­ic assets are things like CSS, JavaScript, images, etc. that don’t require being dynam­i­cal­ly ren­dered. Because we want to lever­age brows­er caching of sta­t­ic resources, we set the expires head­er for these resource to be 30 days or more.

This tells the client brows­er that it can cache these resources local­ly, so that the next time the page is vis­it­ed, it doesn’t have to go out over the wire to down­load them. It already has a cached copy!

Any time we can avoid down­load­ing things alto­geth­er, it’s a big gain — espe­cial­ly on high laten­cy mobile devices with lim­it­ed (and often metered) data bandwidth.

In an ide­al world, these sta­t­ic resources are also deliv­ered by a serv­er geo­graph­i­cal­ly close to the client brows­er, via a Con­tent Deliv­ery Net­work (CDN). Check out the A Pret­ty Web­site Isn’t Enough arti­cle for more per­for­mance-relat­ed 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 brows­er Hey, I know you have this thing cached, which is great, but there’s a new ver­sion of it, and we real­ly would like you to down­load it.”

This is where sta­t­ic asset ver­sion­ing comes in. We want some way to indi­cate the ver­sion of the thing that’s being request­ed, so that if the client brows­er doesn’t have the lat­est ver­sion, it can down­load it. In the past, the query string was used for this, and you’d have some­thing like:

/css/site-css.min.css?v=325329

This is known as query string based cache bust­ing, and while it can work, it doesn’t play that nice with caching, prox­ies and CDNs.

So instead, we can use some­thing called file­name based cache bust­ing, which would look some­thing like this:

/css/site-css.min.325329.css

Since the ver­sion is part of the file­name, a change in the ver­sion looks to the brows­er like a request for an entire­ly dif­fer­ent file.

Link Keep It Simple, Stupid

So great, how do we do this? As with any­thing fron­tend dev relat­ed, there are many ways to accom­plish it. You can use some­thing like gulp-rev to gen­er­ate a man­i­fest file for every sta­t­ic resource, and use that for your ver­sion­ing scheme.

How­ev­er, I try to keep the tool­chain and build sys­tem as sim­ple as pos­si­ble, because I believe in Murphy’s Law:

Anything that can go wrong, will go wrong.

So unless there is a real­ly com­pelling rea­son for a hel­la­cious­ly com­pli­cat­ed sys­tem, I try to avoid it. As with any­thing, there are down­sides to my approach, and I don’t use it for every project. But here’s one way to do it.

A num­ber of peo­ple read the A Bet­ter package.json for the Fron­tend & Imple­ment­ing Crit­i­cal CSS on your web­site arti­cles noticed a fun­ny lit­tle thing in the code: {{ staticAssetsVersion }}

That’s just my sim­ple file­name-based cache bust­ing system.

Link Server Setup

The first thing we need to do is add a sim­ple rule to our serv­er. Remem­ber, the site-css.min.css file is the same file, we’re just using the ver­sion string as a way to bust the cache.

So wouldn’t it be great if we could tell our serv­er to com­plete­ly ignore the ver­sion string, and just serve up the actu­al file? As it turns out, we can do this pret­ty easily.

So any requests that come in for site-css.min.XXXX.css (where the XXXX is a ver­sion num­ber) will cause the serv­er to ignore the ver­sion num­ber, and sim­ply serve up the file site-css.min.css.

For Apache, we do this: 

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.+)\.(\d+)\.(bmp|css|cur|gif|ico|jpe?g|js|png|svgz?|webp|webmanifest)$ $1.$3 [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 web­serv­er, and like mag­ic, any num­bers before the sta­t­ic asset file­name exten­sion are removed. I’ve built this into the Nginx-Craft boil­er­plate, if you’d like to give that a go.

The nice thing about this is that you then don’t need to exclude your sta­t­ic assets from your git repo, because you don’t have hun­dreds of named files clog­ging it up. Each file has one name, it’s just the request URL that changes based on the ver­sion (which is stripped out by the server).

This allows you to elim­i­nate an inter­me­di­ate build step on deploy, so a sim­ple git pull will result in a com­plete­ly func­tion­al website.

If you’re using Lar­avel Valet for local dev, I’ve writ­ten a Local­Valet­Driv­er you can just drop in your project root, and every­thing will just work” from a serv­er-con­fig point of view.

Link Frontend Setup

So then now how do we actu­al­ly change this ver­sion num­ber on the fron­tend, to make sure that it the brows­er cache gets bust­ed when we change our sta­t­ic resources?

So in Craft CMS, we just add an envi­ron­ment vari­able 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 sin­gle envi­ron­men­tal vari­able called staticAssetsVersion that’s set to a num­ber (84 in this case) in all envi­ron­ments. When we make changes that we deploy to pro­duc­tion, we can change this num­ber, push it, and away we go.

At first glance, this seems insane: man­u­al­ly chang­ing any­thing in this day and age seems incred­i­bly backwards.

How­ev­er, the real­i­ty is that actu­al push­es to pro­duc­tion are gen­er­al­ly quite infrequent.

We can also take advan­tage of Gulp to auto­mate 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 major­i­ty of the changes are made in local devel­op­ment, and on staging for the client to approve the changes after they’ve been tested.

In these envi­ron­ments, 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 sim­ply do this to request our sta­t­ic 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 pro­duc­tion, we just bump the num­ber in our general.php file, and deploy the changes to live production.

Link So What Are the Downsides?

One com­mon com­plaint about this rel­a­tive­ly sim­ple approach to sta­t­ic asset ver­sion­ing is that since we have the built sta­t­ic assets such as our CSS in the git repo, how do we han­dle merge conflicts?

Deal­ing with merge con­flicts in a min­i­mized CSS file is a night­mare!” they say.

I think this is most­ly a red her­ring, because the final min­i­mized CSS file is some­thing that’s built from your source. If some­one else com­mit­ted a change, no big deal, accept their ver­sion, then re-build it, and com­mit yours.

You don’t ever need to fig­ure out the diff, or resolve the merge con­flicts at all.

Anoth­er down­side to this sys­tem is that there’s one glob­al ver­sion for every­thing. That means if you only change the CSS, too bad, the JavaScript will be cache bust­ed site-wide as well.

Use whatever system is best for your project; build systems are not a one size fits all proposition.

So clear­ly, this sys­tem is not per­fect. But it does fit the Keep It Sim­ple, Stu­pid (KISS) method­ol­o­gy, and for many sites, it’s all you’re going to end up needing.

There are cer­tain­ly cas­es where a man­i­fest file with per-resource ver­sion­ing is bet­ter, such as if you’re con­stant­ly push­ing changes to live pro­duc­tion, with a large team work­ing togeth­er on the same sta­t­ic assets, and you have a large num­ber of sta­t­ic assets that you’d ide­al­ly like to be indi­vid­u­al­ly cache busted.

But use project-appro­pri­ate tool­ing in your build sys­tem, rather than try­ing to turn every­thing into a ham­mer. You might like to be able to cre­ate a build sys­tem that you nev­er change, and use for every sin­gle site you use, but mag­ic 8-ball says:

Magic 8 Ball

It’s just not that like­ly to hap­pen with the pace of the JavaScript and web­dev worlds. Obvi­ous­ly we want to re-use as much as we pos­si­bly can, and lever­age our past work, but the real­i­ty is that your tool­ing should reflect the project you’re work­ing on, and it’s going to be con­stant­ly chang­ing and evolving.

That’s it folks, enjoy your day.