Cache-busting d'un fichier JSON en PHP : filemtime comme numéro de version

Le listing du blog charge les articles via fetch('posts.json'). Problème : le navigateur met ce fichier en cache. Quand on publie un nouvel article, les visiteurs continuent de voir l'ancienne liste jusqu'à ce qu'ils vident leur cache manuellement — ce qu'ils ne font jamais.

Pourquoi le navigateur cache le JSON

Sans directive Cache-Control explicite, Apache applique une heuristique : il estime une durée de cache à partir du header Last-Modified du fichier. En pratique, les navigateurs peuvent garder le fichier en cache plusieurs heures, voire plusieurs jours selon leurs propres règles.

On pourrait résoudre ça côté serveur avec un header HTTP :

<FilesMatch "posts\.json$">
    Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>

Mais ça nécessite que mod_headers soit activé sur Apache — ce qui n'est pas garanti sur un hébergement mutualisé. Et un no-cache systématique force un aller-retour serveur à chaque chargement de page, même quand le fichier n'a pas changé.

La solution : filemtime comme query param

Le principe du cache-busting par query string est simple : tant que l'URL ne change pas, le navigateur sert depuis son cache. Dès que l'URL change, il refetch.

On injecte le timestamp de dernière modification du fichier via PHP :

fetch('posts.json?v=<?php echo filemtime(__DIR__ . "/posts.json"); ?>')

filemtime() retourne un entier Unix — le timestamp de la dernière modification du fichier sur le disque. Résultat dans le HTML généré :

fetch('posts.json?v=1740268800')

Dès qu'on modifie posts.json (ajout d'un article, correction d'un excerpt), le timestamp change, l'URL change, le navigateur considère que c'est une nouvelle ressource et refetch. Entre deux publications, l'URL est identique → le cache est utilisé, pas de requête inutile.

Pourquoi c'est mieux que no-cache

Avec Cache-Control: no-cache, le navigateur doit interroger le serveur à chaque visite pour savoir si le fichier a changé (requête conditionnelle avec If-Modified-Since). C'est rapide, mais c'est quand même un aller-retour réseau.

Avec le cache-busting par query string, si le fichier n'a pas changé depuis la dernière visite, l'URL est exactement la même — le navigateur sert depuis son cache local, sans aucune requête réseau. C'est plus agressif dans le bon sens : on bénéficie du cache quand c'est possible, on le contourne uniquement quand c'est nécessaire.

La même technique pour les assets CSS et JS

C'est exactement le principe utilisé par tous les bundlers modernes (Vite, Webpack) qui génèrent des noms de fichiers avec hash : main.a3f9c2.js. Ici on n'a pas de build, mais filemtime() joue le même rôle sans aucune infrastructure :

<link rel="stylesheet" href="assets/css/styles.css?v=<?php echo filemtime(__DIR__ . '/assets/css/styles.css'); ?>">
<script src="assets/js/main.js?v=<?php echo filemtime(__DIR__ . '/assets/js/main.js'); ?>"></script>

Les navigateurs et CDN ignorent le query string pour la correspondance de cache dans certaines configurations, mais pour un hébergement Apache standard avec des fichiers servis directement, ça fonctionne de manière fiable sur tous les navigateurs modernes.

Limite : les proxies intermédiaires

Certains proxies et CDN (Cloudflare en mode agressif, Varnish par défaut) ignorent le query string et servent la même version cachée quelle que soit la valeur de ?v=. Dans ce cas, le cache-busting par nom de fichier (hash dans le nom) est plus robuste. Pour un portfolio servi directement par Apache sans CDN, ce n'est pas un problème.

Commentaires (0)