Cache Busting Craft CMS Static Assets in Docker
Just a quick one to show you how I perform lovely cache busting of static assets when using Craft CMS in Docker.
I case you're new to this: I'm talking about how we can stop a user's browser from re-using a locally cached version of a CSS, JS or image file instead of downloading it again when we have updated that file on the server.
If you've followed along with my Craft in Docker series you'll know that I use GitLab CI to build my docker images ready for deployment. You'll also know that I go on a lot about those images being static in nature.
A few things to keep in mind:
- In the docker in production workflow we are forced to build new images each time we want to deploy an update.
- Our static assets are contained within these images.
- We probably want to use filename or get variable based cache busting as it's the simplest to implement.
We can leverage all of these facts to create a simple, fool-proof cache busting system all based around docker image builds.
Create a Macro
We'll use a Twig macro to inject our assets revisions into our HTML. Add the following to your Craft project's templates. I've added mine to templates/_macros/_cache.twig:
{% macro cachebust(path) %}{{ path }}?v={{ random(1, 1000000000) }}{% endmacro %}
We can include this macro in any file that we want to cache bust like so:
{% import '_macros/_cache' as m_cache %}
<link rel="stylesheet" type="text/css" href="{{ m_cache.cachebust('/assets/css/app.css') }}">
If you think that this might be the stupidest cache busting macro you've ever seen, you'd be right. Currently it just cache busts for every request which is bad for production, but useful for local development.
Add the following to your CI script (.gitlab-ci.yaml in my case) just before building your images:
echo "{% macro cachebust(path) %}{{ path }}?v="$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)"{% endmacro %}" > src/templates/_macros/_cache.twig
That's the magic bit. This will overwrite the _cache.twig file that is inserted into your built images with something similar to this:
{% macro cachebust(path) %}{{ path }}?v=DXI9ODh4s5umqfH2GbXaVZSiSQjDGyNW{% endmacro %}
The version string is obviously randomised for each image build. You can think of this random string as an identifier for the version of the entire image and by appending it to all of our static assets we ensure that whenever we put a new image live all of our users will download any updated static assets.
You can further improve this by adjusting the macro to include the version within the filename, something like app.8763846586743.css would work and then using a regex based rewrite in nginx to make sure the correct file is served. But this is only strictly necessary if you're serving via a CDN which doesn't index against GET variables. So maybe don't bother ¯\_(ツ)_/¯
This very website is using this cache busting mechanism. If you have a look at the HTML you'll be able to see where I've used the macro and you'll be able to verify that each time I've used it it has output the same version string.