I’m quite chuffed with this! I’ve been solving this in different ways for a heck of a lot of years, in situations where asset cache busting isn’t part of whatever site building situation Iโm in provides so I need to do it myself. But it always requires a little more technology than I would like, like setting up Gulp or the like.
This is all it is:

The Setup
You’ve got a stylesheet on a website. So in the HTML:
<link rel="stylesheet" href="style.css">
Code language: HTML, XML (xml)
I’ve got my server/CDN set up to cache the living bejesus out of that file. That’s a no-brainer day-one kind of performance requirement. So if I check the response headers on a file like that, I’ll see something like:
cache-control: public, max-age=31536000
Code language: PHP (php)
So when I need to change that file, I need to break that cache, otherwise, users’ browsers that have already been to the site will hang onto their old version of file. That’s worse than a failed deployment, it’s almost like a half/broken deployment because it’s likely HTML has also changed that no longer correctly corresponds to the updated CSS. That’s bad.
A classic easy way to break cache is to add a query parameter:
<link rel="stylesheet" href="style.css<strong>?version=2</strong>">
Code language: HTML, XML (xml)
Query parameters are fineยน.
The code we’re looking at? That’s HTML. So the job is:
๏
Automatically update the version number in the HTML when a new deployment is made that changes the contents of that file.
One possible system: update the query parameters by hand. It’s not that hard of a task, but it’s easy to forget, and the consequences of forgetting, as I mentioned above, suck pretty hard.
Aren’t there lots of ways to do this?
There are (checks notes) a million ways to do this. It’s such a known need that it’s built into all sorts of software. The Rails asset pipeline does this. Bundlers like Rollup, Parcel, Vite, etc will happily do this as well. The assumption in all of these is that it’s processing your HTML and producing a post-processed version of the HTML that is deployed.
Most commonly, I reach for Gulp.
<strong>// For example, I'll set up a watch task for scripts
</strong>gulp.watch(LOCATION_OF_SCRIPTS, doScripts);
<strong>// Then I'll do a bunch of stuff to them when they change.
</strong>function doScripts(done) {
return gulp.series(
preprocessJs,
concatJs,
minifyJs,
deleteArtifactJs,
reload,
done => {
cacheBust("./parts/footer-scripts.php", "./parts/");
done();
}
)(done);
}
<strong>// At the end, I'll do a Find & Replace on the cache busting string in the PHP file.
</strong>function cacheBust(src, dest) {
var cbString = new Date().getTime();
return gulp
.src(src)
.pipe(
replace(/cache_bust=\d+/g, function() {
return "cache_bust=" + cbString;
})
)
.pipe(gulp.dest(dest));
}
Code language: PHP (php)
To me, assuming that HTML (via PHP or not) will be processed is a fairly big assumption. I have worked on lots of websites that don’t have anything processing the HTML. Or even if there is some kind of HTML processor in place, it doesn’t have an auto-incrementing cache buster automatically in place. Like a basic SSG or whatnot.
So when do we need this?
But here’s my #1 use case: WordPress websites.
A WordPress website produces HTML from PHP templates. There is no processing there other than PHP itselfยฒ. I tend not to have a build process for WordPress sitesยณ.
Using Buddy for the Find & Replace
So here’s my actual line of code in my header.php
file on a WordPress site.
<link rel="stylesheet" href="<?php bloginfo('stylesheet_url'); ?>?v=<strong>{{{version}}}</strong>">
Code language: HTML, XML (xml)
Note the {{{version}}}
in there. That’s just an invented syntax. Just something fairly unique that I’ll have Buddy replace.
Now here’s an example Buddy pipeline. A lot of times, I’ll have one action: just deploy the dang site. But here we’ll add a Find & Replace setup first.

Within the settings for Find & Replace, we get to tell it what file to run it on, what to look for, and what to replace it with. Miraculously, there are magical variables you have access to (like your ENV vars) including one called $BUDDY_EXECUTION_ID
which is a unique ID per deployment, so it’ll work great for cache busting.

In order for this to work, you need to have the Upload files action set to use Pipeline Filesystem and not GitHub. It’ll still pull from GitHub, but apparently, this gives it permission to alter the files before uploading or something. This is crucial and tripped me up for quite a while setting this up. If you don’t do it, it seems like it is all working, but it just doesn’t work, the file uploads unchanged.

You could run the Find & Replace on every deployment, breaking the cache every time, but I’ve added a conditional here to only do it when the actual stylesheet has changed:

Seeing it Run

You can see the Find & Replace execute and succeed in the pipeline logs.
That’ll do it. I can use this on like 3-4 WordPress websites right now, including this one, so I’m happy to have found this simple solution.
- There used to be one odd-ball server a million years ago that didn’t respect that and required you to change the name of the file, but let’s not worry about that.
- This kinda makes me think that we could do the asset versioning by outputting
filemtime($filename)
at the end of them, which is the last changed time for the file. I’d just want to make sure good HTML caching is in place then so the server isn’t like hitting the disk for that information on every page view. Probably a can of worms there. - If I work on it alone, I tend to use CodeKit just for a little easy Sass action. It has a cache buster too, but only for HTML that is processed, and probably wouldn’t work for PHP.
For those who want a faster solution, they can use the file’s last edited time (timestamp) as the version number as follows:
<link href="/css/style.min.css?v=” rel=”stylesheet”>
This will always version the file once for the last edited time.
We use the
filemtime($filename)
method you mention in the footnotes for styles and js cache busting in WordPress. It works well to always bust when the asset actually changes. With the linux filesystem caches on the server the read only needs to happen in memory most of the time so performance impact is negligible especially compared to all of the file reads that WordPress already does. Also, with page caching the result of it is cached for most page loads anyway.You can use a similar approach with GitHub Actions: you can create an action to SSH into the VPS where the site’s been deployed, then use sed to swap out the {{{version}}} magic string with the action’s run number (github.run_number).