L’alerte est arrivée par un simple message d’un collègue en télétravail : « la date de publication clignote sur la page article, elle affiche d’abord une heure puis une autre ». Un clignotement d’une fraction de seconde, invisible pour la plupart des visiteurs mais bien réel, et surtout révélateur d’un problème plus profond qu’un simple souci d’affichage.
Le site, un blog d’actualité juridique construit avec Next.js et alimenté par un WordPress headless, générait ses pages articles en statique au moment du build, avec une régénération incrémentale toutes les heures. La date de publication de chaque article, renvoyée par l’API REST WordPress au format ISO 8601 (2024-03-08T14:30:00), était transformée côté React en une date lisible du type « 8 mars 2024 à 14h30 ».
Le symptôme dans la console
En ouvrant la console développeur sur la page en production, un avertissement React apparaissait clairement : Text content did not match. Server: "8 mars 2024 à 15h30" Client: "8 mars 2024 à 14h30". Une différence d’exactement une heure entre le rendu généré côté serveur au moment du build et le rendu recalculé côté client au moment de l’hydratation.
Ce type d’avertissement est l’un des plus fréquents sur les sites headless utilisant le rendu statique ou serveur de React, et il est souvent traité trop vite comme un simple bruit sans conséquence. En réalité, chaque mismatch d’hydratation force React à jeter le rendu serveur pour le composant concerné et à le reconstruire entièrement côté client, ce qui dégrade la performance perçue et peut, dans des cas plus graves, provoquer un contenu visible incohérent le temps du recalcul.
Le diagnostic
La fonction de formatage utilisée s’appuyait sur l’objet Date natif de JavaScript et sur toLocaleString, sans préciser explicitement de fuseau horaire :

function formaterDate(dateIso) {
const d = new Date(dateIso);
return d.toLocaleString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
Le serveur de build de la plateforme d’hébergement fonctionnait en temps universel coordonné (UTC), tandis que le navigateur du visiteur (et celui du développeur qui a repéré le bug) appliquait le fuseau Europe/Paris, en avance d’une heure sur l’UTC en mars, avant le passage à l’heure d’été. La date brute WordPress, elle, ne précisait pas explicitement son fuseau dans la chaîne ISO renvoyée par l’API REST (absence du suffixe Z ou d’un décalage explicite), ce qui laissait toLocaleString appliquer le fuseau local de l’environnement d’exécution, différent entre le serveur de build et le navigateur.
Pourquoi WordPress renvoie une date ambiguë
L’API REST WordPress expose en réalité deux champs de date distincts pour chaque article : date, qui correspond à l’heure locale configurée dans les réglages du site, et date_gmt, qui correspond à la même date convertie en temps universel. Le champ date, utilisé par erreur ici, n’inclut aucune information de fuseau dans sa chaîne, ce qui le rend ambigu dès qu’il est interprété par un environnement JavaScript dont le fuseau local diffère de celui configuré dans WordPress.
Le correctif
Deux changements ont été nécessaires. D’abord, utiliser systématiquement date_gmt plutôt que date pour toute donnée transformée côté JavaScript, en ajoutant explicitement le suffixe Z pour lever toute ambiguïté :
function formaterDate(dateGmt) {
const d = new Date(dateGmt + 'Z');
return d.toLocaleString('fr-FR', {
timeZone: 'Europe/Paris',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
Ensuite, et c’est le correctif le plus robuste sur le long terme, le formatage de toute donnée temporelle sensible au fuseau a été déplacé dans un composant qui ne s’exécute qu’après le montage côté client, via un état initialisé à une chaîne vide puis mis à jour dans un useEffect :
function DateArticle({ dateGmt }) {
const [texte, setTexte] = useState('');
useEffect(() => {
setTexte(formaterDate(dateGmt));
}, [dateGmt]);
return <time dateTime={dateGmt}>{texte || '—'}</time>;
}
Cette dernière approche accepte un très bref affichage d’un tiret le temps de l’hydratation, mais élimine toute possibilité de mismatch, puisque le rendu serveur ne tente plus jamais de deviner un fuseau qu’il ne connaît pas de façon fiable.
Prévention sur les projets suivants
- Toujours préférer
date_gmtàdatepour toute valeur temporelle manipulée en JavaScript sur un projet headless. - Préciser explicitement le fuseau horaire cible dans tout appel à
toLocaleStringou à une bibliothèque de date, sans jamais compter sur le fuseau implicite de l’environnement d’exécution. - Traiter tout avertissement d’hydratation React comme un signal à corriger, même quand l’effet visuel semble anodin : il révèle presque toujours une divergence d’environnement bien réelle.
Un mismatch d’hydratation qui ne casse rien visuellement aujourd’hui est souvent celui qui cassera quelque chose demain, sur un autre composant, dans un fuseau horaire différent.
En résumé
Ce bug n’avait rien d’exotique une fois isolé, mais il illustre bien un piège classique du rendu hybride : le serveur qui construit la page et le navigateur qui l’affiche ne partagent pas nécessairement le même contexte d’exécution, et une donnée aussi anodine qu’une date peut suffire à le révéler. Sur un projet headless consommant WordPress, la discipline consiste à toujours privilégier les champs explicitement non ambigus de l’API, comme date_gmt, plutôt que leurs équivalents locaux plus pratiques en apparence mais dépendants du contexte.