SEO-004
Cache assets forever and bust them with filemtime
A version query derived from the file's own modification time. No build step, no manual version numbers, and no user stuck on last week's stylesheet.
Caching a stylesheet for a year is the right thing to do for speed and the wrong thing to do for deployment, unless the URL changes when the file does.
The file's modification time is already a perfect version marker. It changes exactly when the content changes, requires nothing to maintain, and needs no build tooling — which matters when the site is going to be uploaded over FTP by someone who has never run npm.
Cache the filemtime call itself for the request. On a page rendering several assets, the stat calls add up for no reason.
function asset(string $path): string
{
static $cache = [];
$path = ltrim($path, '/');
if (!isset($cache[$path])) {
$full = TD_ROOT . '/' . $path;
// Fall back to a per-deploy constant if the file is unreadable.
$cache[$path] = is_file($full) ? (string) filemtime($full) : (string) APP_VERSION;
}
return url($path) . '?v=' . $cache[$path];
}
// <link rel="stylesheet" href="<?= e(asset('assets/css/app.css')) ?>">
// <script src="<?= e(asset('assets/js/app.js')) ?>" defer></script>
Using it
Pair it with a long cache header, which is the half that actually makes it worthwhile:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
Uploading a changed file gives it a new mtime, which gives it a new URL, which every browser fetches fresh. Nothing else to do.
What bites people
FTP clients that preserve modification times will not bust the cache. Turn that option off, or touch the files after upload.
A missing file returns false from filemtime and emits a warning. The is_file guard above is not optional on a live site.
Query-string versioning is not honoured by every intermediate proxy. Filename versioning is stricter, but it needs a build step — which is exactly what this approach avoids.