On a vu un site e-commerce perdre 70% de ses résultats enrichis en 48 heures après une migration vers App Router avec rendu partiellement client. Le JSON-LD était présent dans la source HTML, le validateur le confirmait, aucune erreur dans le rapport d’enrichissement. Pourtant Google affichait les URLs comme des liens bleus classiques, sans étoiles, sans prix, sans breadcrumb. Le problème ne venait pas du schéma, mais de la manière dont Next.js livrait ce bloc de données au moment où Googlebot le réclamait.
Le piège du validateur Google
L’outil de test des données structurées de Google fonctionne sur un snapshot statique du DOM, pas sur le rendu final après exécution du JavaScript dans les conditions réelles du crawler. Résultat : un JSON-LD injecté après hydratation via un useEffect peut apparaître dans le validateur si vous y collez le source, mais rester invisible pour Googlebot si le rendu mobile ne l’a jamais rencontré.
C’est le scénario type du faux positif qui fait croire à l’équipe SEO que tout est en ordre, alors que le crawler ne voit qu’une coquille vide.
JSON-LD vs microdonnées : le débat qui masque le vrai problème
On oppose souvent JSON-LD aux microdonnées intégrées au HTML comme si le format déterminait le succès. C’est un faux débat. Les systèmes de classement traitent les deux avec la même logique d’extraction, pourvu que le contenu soit cohérent avec la page visible. La différence qui importe, c’est la résilience de votre pipeline de livraison.
Les microdonnées, parce qu’elles sont incrustées dans les balises HTML existantes, survivent à la plupart des chaos d’hydratation. Elles arrivent en même temps que le contenu, sans bifurcation de flux. Le JSON-LD, lui, est un bloc script autonome. S’il n’est pas sérialisé dans le flux HTML au moment exact où le serveur envoie les premiers octets, il devient tributaire du timing d’exécution du JavaScript côté client. Et ce timing, avec Googlebot Mobile, est tout sauf prévisible.
Quand le rendu côté client efface vos données structurées
Prenons un composant produit classique dans Next.js App Router. On pourrait croire qu’un <script type="application/ld+json"> injecté dans le contenu retourné par une fonction asynchrone fera toujours l’affaire. En réalité, si ce script est généré dans un composant client, il ne sera pas présent dans le flux HTML initial servi au bot. Il n’apparaîtra qu’après re-render, une fois que le bundle JS aura été téléchargé, parsé, exécuté.
Googlebot Mobile a une fenêtre d’exécution très courte pour le JavaScript. Quand le crawl est rapide, il peut arrêter le traitement avant que votre JSON-LD n’ait eu le temps d’exister dans le DOM. Voilà comment des milliers de fiches produit se retrouvent sans rich snippet, malgré une Search Console silencieuse. On a passé deux jours à inspecter des logs de rendu custom pour tomber sur cette conclusion : le JSON-LD était présent dans la réponse HTML après chargement dynamique, mais absent du snapshot pris par le crawler à sa fenêtre de rendu.
La solution n’est pas d’abandonner le JSON-LD au profit des microdonnées partout. Elle consiste à s’assurer que le bloc application/ld+json soit émis côté serveur, directement dans le flux initial, sans dépendre d’un cycle de vie React. Dans App Router, cela passe par un composant serveur qui sérialise le schéma et l’insère dans le <head> avant tout comportement client.
// Exemple simplifié pour une fiche produit avec App Router
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.slug);
const schema = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'EUR',
availability: 'https://schema.org/InStock',
},
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
<div>{/* contenu du produit */}</div>
</>
);
}
Tout ce qui diffère l’injection à un effet client rend le JSON-LD vulnérable. Sur des pages à fort trafic, ces erreurs passent sous les radars : elles sont intermittentes, dépendantes de la rapidité du crawl et de la charge serveur. Côté perf, un JSON-LD injecté trop tard peut retarder le First Contentful Paint quand le navigateur interrompt l’analyse du flux pour traiter un script inconnu.
Injection robuste dans Next.js : le pattern qui ne casse pas l’hydratation
Le réflexe d’un développeur front enlisé dans une logique de state management consiste parfois à pousser le JSON-LD dans un store Zustand ou un contexte React global. C’est une erreur. Non seulement le store n’est disponible qu’après hydratation, mais il introduit une couche d’abstraction qui rend le débogage du rendu encore plus obscur. Le choix d’un state management léger comme Zustand a justement l’avantage de pouvoir rester simple, mais l’injecter dans la chaîne de livraison des données structurées revient à faire dépendre une information statique serveur d’un runtime client. La seule voie fiable : traiter le JSON-LD comme une donnée immuable de la réponse HTTP, pas comme un élément d’interface.
Quand on architecture un site à grand volume, on réserve un slot déterministe dans le flux serveur, avant toute hydratation. Certains ajoutent même un flag dans les logs de build pour s’assurer que le schéma est bien présent à chaque déploiement. Ça évite les régressions silencieuses après une mise à jour de dépendance qui modifie le comportement de rendu.
Le script inline tue ton TTFB
Un schéma d’accueil qui embarque plusieurs centaines de lignes (organisation, site, sameAs, search action) gonfle le flux HTML. Sur un Node saturé, ce bloc inline retarde le premier octet utile. Supprimer les WebSite ou Organization redondants sur les pages de détail rend 20 à 40ms de TTFB.
Ce que voit Googlebot, depuis le terminal
Le validateur ne sert plus à grand-chose pour un audit de production. Un curl avec le user-agent Googlebot Smartphone ramène le HTML brut servi en première réponse.
curl -s -H "User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.264 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://votre-site.com/produit
Dans la réponse, le application/ld+json doit apparaître avant toute balise </script> liée à un bundle client. Sinon, il y a un problème. Un outil headless comme Puppeteer permet ensuite d’enregistrer le rendu complet en simulant les conditions matérielles du bot mobile (latence réseau, limitation CPU). C’est comme ça qu’on a trouvé notre erreur d’hydratation : la page finissait par contenir le JSON-LD après 4,3 secondes de traitement JS, soit bien au-delà de la fenêtre du crawler.
Un test de non-régression qui parse le HTML statique et vérifie la présence du script structuré pour chaque type de page clé épargne des semaines de sur-crawl sans enrichissement.
Questions fréquentes
Les microdonnées HTML sont-elles plus rapides à traiter par Google que le JSON-LD ? Non, la vitesse d’extraction est similaire. La différence se situe dans la robustesse de livraison : les microdonnées intégrées dans le markup éliminent le risque d’un découplage entre injection et rendu, ce qui les rend plus résistantes aux erreurs d’hydratation.
Faut-il injecter le JSON-LD dans le <head> ou le <body> ?
La spécification ne l’interdit pas dans le <body>, mais le placer dans le <head> garantit que Googlebot le lise avant tout contenu dynamique lourd. Si votre pipeline serveur le permet, restez dans le <head> pour minimiser les risques d’interruption.
Peut-on mixer microdonnées et JSON-LD sur une même page ? Techniquement oui, mais c’est un signal de dette technique. En cas d’incohérence entre les deux, Google retient la version la plus défavorable. Choisissez un format par page, maintenez-le parfaitement aligné avec le contenu visible, et testez-le en conditions réelles.