Un site e-commerce de 12 000 fiches produit migre ses données structurées de Microdata vers JSON-LD, corrige ses AggregateRating invalides et passe de 800 à 6 400 pages éligibles aux rich results en Search Console — en trois semaines. La différence entre un Product Schema qui déclenche des étoiles dans les SERP et un balisage ignoré par Google tient souvent à une poignée de propriétés manquantes ou mal formatées.
Ce que Google attend réellement d'un Product Schema
La documentation officielle de Google sur le Product markup distingue deux niveaux d'éligibilité. Le premier : les snippets de prix (avec Offer). Le second : les étoiles d'avis (avec AggregateRating ou Review). Chaque niveau a ses propriétés requises et recommandées, et les confondre est la première source d'erreurs.
Propriétés requises vs recommandées : le vrai impact
Google ne va pas vous afficher de rich result si name est absent. C'est évident. Ce qui l'est moins : une fiche avec Offer mais sans priceCurrency sera ignorée silencieusement. Pas d'erreur dans le rapport Rich Results de Search Console — juste une absence de résultat. Le rapport fait la distinction entre "invalide" (erreur bloquante) et "valide avec avertissements", mais il ne vous dit pas pourquoi un balisage techniquement valide ne génère aucun rich result.
Les propriétés qui font la différence en pratique :
offers.priceetoffers.priceCurrency: indissociables. Un prix sans devise est inutilisable.offers.availability: Google utilise cette valeur pour filtrer les produits en rupture. UnOutOfStockpermanent finit par faire disparaître le rich result.aggregateRating.ratingCount: sans le nombre d'avis, Google considère le rating comme non fiable. UnratingValueseul ne suffit pas.offers.priceValidUntil: souvent ignorée, cette propriété signale à Google que le prix affiché est temporaire (promo). Sans elle, un prix barré dans le balisage sans date de fin peut être considéré comme trompeur.
Le piège du Product sans Offer
Un pattern qu'on voit souvent sur les marketplaces : le balisage Product existe, mais le prix est injecté côté client via JavaScript après un appel API pour vérifier le stock. Si Googlebot ne voit pas l'Offer au crawl, le Product Schema est considéré comme incomplet. Pas de rich result prix — même si le AggregateRating est correct.
Implémentation JSON-LD complète : du cas simple au multi-vendeur
Le JSON-LD est le format recommandé par Google pour les données structurées. Si vous utilisez encore Microdata sur vos fiches produit, la migration vers JSON-LD simplifie la maintenance et découple le balisage structuré du HTML visible. Pour les fondamentaux du format, consultez notre guide pratique JSON-LD.
Fiche produit standard avec avis agrégés
Voici un balisage complet pour une fiche produit e-commerce classique — un casque audio vendu en direct, avec des avis clients :
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Casque audio sans fil ProSound X7",
"image": [
"https://www.audiostore.fr/images/prosound-x7-front.jpg",
"https://www.audiostore.fr/images/prosound-x7-side.jpg"
],
"description": "Casque Bluetooth 5.3 avec réduction de bruit active, autonomie 40h, codec aptX HD.",
"sku": "PSX7-BLK-2025",
"gtin13": "3700000000123",
"brand": {
"@type": "Brand",
"name": "ProSound"
},
"offers": {
"@type": "Offer",
"url": "https://www.audiostore.fr/casques/prosound-x7",
"priceCurrency": "EUR",
"price": "189.99",
"priceValidUntil": "2026-06-30",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"seller": {
"@type": "Organization",
"name": "AudioStore"
},
"shippingDetails": {
"@type": "OfferShippingDetails",
"shippingRate": {
"@type": "MonetaryAmount",
"value": "0",
"currency": "EUR"
},
"deliveryTime": {
"@type": "ShippingDeliveryTime",
"handlingTime": {
"@type": "QuantitativeValue",
"minValue": 0,
"maxValue": 1,
"unitCode": "DAY"
},
"transitTime": {
"@type": "QuantitativeValue",
"minValue": 1,
"maxValue": 3,
"unitCode": "DAY"
}
},
"shippingDestination": {
"@type": "DefinedRegion",
"addressCountry": "FR"
}
},
"hasMerchantReturnPolicy": {
"@type": "MerchantReturnPolicy",
"applicableCountry": "FR",
"returnPolicyCategory": "https://schema.org/MerchantReturnFiniteReturnWindow",
"merchantReturnDays": 30,
"returnMethod": "https://schema.org/ReturnByMail",
"returnFees": "https://schema.org/FreeReturn"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.6",
"reviewCount": "347",
"bestRating": "5",
"worstRating": "1"
},
"review": [
{
"@type": "Review",
"author": {
"@type": "Person",
"name": "Marie L."
},
"datePublished": "2026-03-12",
"reviewBody": "Excellent rapport qualité/prix. La réduction de bruit est bluffante pour cette gamme de prix.",
"reviewRating": {
"@type": "Rating",
"ratingValue": "5",
"bestRating": "5"
}
}
]
}
Quelques points à noter sur ce balisage :
shippingDetails et hasMerchantReturnPolicy sont devenus des propriétés recommandées depuis 2023. Google les utilise pour les résultats enrichis dans Google Shopping et les organic product listings. Sans eux, vous êtes éligible aux étoiles et au prix dans les SERP classiques, mais vous ratez les emplacements enrichis du Merchant Center gratuit.
gtin13 (ou gtin8, gtin14, isbn) est techniquement optionnel, mais Google a explicitement indiqué que les produits avec un identifiant global sont prioritaires pour le Product Knowledge Panel. Si vos produits ont un code-barres EAN, ne pas le déclarer est une perte sèche.
price en string vs number : les deux fonctionnent en JSON-LD, mais utilisez un format décimal avec point (pas de virgule, pas de symbole €). "189.99" est correct. "189,99€" sera rejeté.
Cas multi-vendeur : AggregateOffer
Sur une marketplace où plusieurs vendeurs proposent le même produit à des prix différents, le type AggregateOffer permet de déclarer une fourchette de prix :
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Casque audio sans fil ProSound X7",
"sku": "PSX7-BLK-2025",
"brand": {
"@type": "Brand",
"name": "ProSound"
},
"offers": {
"@type": "AggregateOffer",
"lowPrice": "169.90",
"highPrice": "219.00",
"priceCurrency": "EUR",
"offerCount": "5",
"availability": "https://schema.org/InStock",
"offers": [
{
"@type": "Offer",
"url": "https://www.marketplace.fr/prosound-x7?seller=audiostore",
"price": "189.99",
"priceCurrency": "EUR",
"seller": {
"@type": "Organization",
"name": "AudioStore"
},
"availability": "https://schema.org/InStock"
},
{
"@type": "Offer",
"url": "https://www.marketplace.fr/prosound-x7?seller=techzone",
"price": "169.90",
"priceCurrency": "EUR",
"seller": {
"@type": "Organization",
"name": "TechZone"
},
"availability": "https://schema.org/InStock"
}
]
}
}
Le trade-off de AggregateOffer : Google affiche "à partir de 169,90 €" dans les SERP, ce qui peut augmenter le CTR quand votre prix bas est compétitif, mais peut aussi créer de la frustration si l'offre à ce prix est en rupture. Maintenez la cohérence entre le prix déclaré et le prix réellement accessible sur la page.
Gestion dynamique du balisage avec un framework JS
La majorité des e-commerces modernes tournent sur Next.js, Nuxt ou un framework headless. Le balisage JSON-LD doit être injecté côté serveur (SSR) pour que Googlebot le voie systématiquement au premier crawl, sans dépendre de l'exécution JavaScript.
Injection SSR avec Next.js (App Router)
// app/produits/[slug]/page.tsx
import { Metadata } from 'next';
interface ProductData {
name: string;
sku: string;
gtin: string;
price: number;
currency: string;
availability: 'InStock' | 'OutOfStock' | 'PreOrder';
ratingValue: number;
reviewCount: number;
imageUrls: string[];
description: string;
brandName: string;
}
async function getProduct(slug: string): Promise<ProductData> {
const res = await fetch(`${process.env.API_URL}/products/${slug}`, {
next: { revalidate: 300 } // ISR : revalide toutes les 5 minutes
});
if (!res.ok) throw new Error('Product not found');
return res.json();
}
function buildProductJsonLd(product: ProductData, slug: string) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.imageUrls,
description: product.description,
sku: product.sku,
gtin13: product.gtin,
brand: {
'@type': 'Brand',
name: product.brandName,
},
offers: {
'@type': 'Offer',
url: `https://www.audiostore.fr/produits/${slug}`,
priceCurrency: product.currency,
price: product.price.toFixed(2),
availability: `https://schema.org/${product.availability}`,
itemCondition: 'https://schema.org/NewCondition',
},
...(product.reviewCount > 0 && {
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: product.ratingValue.toFixed(1),
reviewCount: product.reviewCount.toString(),
bestRating: '5',
worstRating: '1',
},
}),
};
}
export default async function ProductPage({
params
}: {
params: { slug: string }
}) {
const product = await getProduct(params.slug);
const jsonLd = buildProductJsonLd(product, params.slug);
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* Rendu de la page produit */}
</>
);
}
Trois éléments critiques dans ce code :
Le revalidate: 300 (ISR) est un compromis entre fraîcheur des prix et charge serveur. Pour un catalogue de 12 000 produits, un revalidate de 5 minutes signifie que le prix affiché à Google peut être décalé de 5 minutes par rapport au stock réel. Acceptable pour la plupart des cas, mais si vous faites du flash sale avec des changements toutes les minutes, réduisez cette valeur ou passez en SSR pur (cache: 'no-store').
Le conditionnel sur aggregateRating : un produit neuf sans aucun avis ne doit pas avoir de bloc AggregateRating avec reviewCount: 0. Google signalera une erreur. Injectez le bloc uniquement quand des avis existent réellement.
price.toFixed(2) : garantit le format décimal attendu par Google, même si l'API renvoie un entier (149 au lieu de 149.00).
Le piège du client-side rendering
Si votre balisage est généré uniquement en JavaScript côté client (SPA React classique sans SSR), Googlebot peut théoriquement l'exécuter. En pratique, sur un crawl massif d'un catalogue de 12 000 pages, le budget de rendering de Google est fini. Certaines pages ne seront pas rendues, ou le seront avec un délai de plusieurs jours. Résultat : un balisage Product qui existe dans le DOM final mais que Google n'a jamais vu.
Vérifiez systématiquement le rendu via l'outil d'inspection d'URL dans Search Console : l'onglet "HTML rendu" vous montre exactement ce que Googlebot a capturé. Si le JSON-LD n'y apparaît pas, votre balisage est invisible.
Validation, debugging et erreurs fréquentes
Workflow de validation en 3 étapes
Étape 1 : Rich Results Test — L'outil Rich Results Test de Google est le seul qui reflète exactement la logique d'éligibilité de Google. Schema.org Validator vérifie la conformité au vocabulaire, mais ne vous dit pas si Google déclenchera un rich result. Ce sont deux choses différentes.
Étape 2 : Search Console > Améliorations > Produits — Ce rapport agrège les erreurs sur l'ensemble du site. C'est ici que vous verrez si 3 000 de vos 12 000 fiches ont un AggregateRating sans reviewCount. Le rapport distingue les erreurs (bloquantes) et les avertissements (non bloquants mais qui réduisent l'éligibilité).
Étape 3 : Crawl Screaming Frog avec extraction custom — Configurez une extraction Custom > JSON-LD pour vérifier la présence et la structure du balisage à grande échelle :
Configuration Screaming Frog :
Mode : List (importez vos URLs produit depuis le sitemap)
Config > Spider > Rendering : JavaScript
Custom Extraction :
- Nom : "product_name"
Selector : script[type="application/ld+json"]
Regex : "name"\s*:\s*"([^"]+)"
- Nom : "product_price"
Selector : script[type="application/ld+json"]
Regex : "price"\s*:\s*"?([\d.]+)"?
- Nom : "rating_count"
Selector : script[type="application/ld+json"]
Regex : "reviewCount"\s*:\s*"?(\d+)"?
L'export CSV vous donne un tableau avec chaque URL et ses valeurs extraites. Filtrez les lignes où product_price est vide — ce sont vos fiches sans Offer parseable. Ce crawl en mode JavaScript est lent (comptez 4-5 heures pour 12 000 URLs avec rendering activé), mais c'est le seul moyen de détecter les fiches où le JSON-LD est injecté côté client sans SSR.
Les 5 erreurs les plus fréquentes (et comment les corriger)
1. review sans author — Depuis septembre 2023, Google exige un author sur chaque Review. Un avis anonyme sans author.name est ignoré. Si vos avis proviennent d'un widget tiers (Avis Vérifiés, Trustpilot), vérifiez que le script d'injection inclut bien l'auteur dans le JSON-LD. Beaucoup de widgets injectent les étoiles visuellement mais pas dans le balisage structuré.
2. Incohérence prix balisage vs page visible — Google compare le prix déclaré dans le JSON-LD avec le prix détecté visuellement sur la page. Si votre balisage indique 189,99 € mais que le prix affiché est 209,99 € (parce que la promo a expiré mais le cache ISR n'a pas été purgé), Google peut déclencher une action manuelle pour données structurées trompeuses. C'est un des cas où un outil de monitoring comme Seogard apporte une vraie valeur : la détection automatique d'incohérences entre le balisage structuré et le contenu visible, avant que Google ne sévisse.
3. availability en dur au lieu de dynamique — Coder "availability": "https://schema.org/InStock" en dur dans un template alors que le produit est en rupture. L'availability doit être pilotée par l'état réel du stock.
4. Doublon de blocs JSON-LD — Un CMS qui injecte un bloc Product dans le thème, plus un plugin SEO (Yoast, RankMath) qui en ajoute un second. Googlebot voit deux déclarations Product sur la même page, avec potentiellement des données contradictoires. Vérifiez avec DevTools (F12 > Console > document.querySelectorAll('script[type="application/ld+json"]')) le nombre de blocs présents.
5. AggregateRating sur des pages listing — Google autorise le Product Schema uniquement sur les pages de produit individuelles. Placer un AggregateRating sur une page catégorie listant 50 produits est une violation des guidelines qui peut entraîner une action manuelle sur l'ensemble du site.
Scénario réel : migration Microdata → JSON-LD sur 15 000 fiches
Un e-commerce spécialisé en équipement outdoor — 15 200 fiches produit, stack Magento 2 avec template Hyvä, environ 90 000 sessions organiques mensuelles.
État initial
Le balisage existant était en Microdata, intégré dans le HTML du template de fiche produit. Problèmes identifiés :
- 4 200 fiches avec
itemprop="price"mais sansitemprop="priceCurrency"→ prix ignorés par Google itemprop="aggregateRating"présent sur les pages catégories (violation des guidelines)- Aucun
shippingDetailsnireturnPolicy - Search Console : 2 100 pages avec erreurs Product, 800 pages valides avec avertissements, 6 300 pages "valides" — mais seulement 800 déclenchant un rich result effectif
Migration
Semaine 1 : développement d'un module Magento custom qui génère le JSON-LD à partir des données produit en base. Le bloc <script type="application/ld+json"> est injecté dans le <head> via un layout XML dédié (catalog_product_view.xml). Suppression simultanée de tous les attributs Microdata du template pour éviter les doublons.
Semaine 2 : déploiement progressif par catégorie (commencer par les catégories les plus crawlées, identifiées via les logs serveur). Validation par lot avec Screaming Frog en mode liste — chaque lot de 500 URLs est crawlé avec rendering JavaScript pour confirmer la présence du JSON-LD.
Semaine 3 : soumission d'un sitemap dédié aux fiches produit pour accélérer le recrawl. Monitoring du rapport "Produits" dans Search Console. Nettoyage de l'AggregateRating sur les pages catégories.
Résultats à 6 semaines
- Pages éligibles rich results : 800 → 6 400
- Impressions SERP avec rich result produit : +340% (mesuré via le rapport Performance filtré sur les apparences "Product result")
- CTR moyen sur les fiches produit en organique : 2.1% → 3.8%
- Aucune action manuelle — la suppression proactive des balisages sur les pages catégories a évité un signalement
Le point clé : le gain ne venait pas d'un meilleur classement (les positions n'ont quasiment pas bougé), mais de l'augmentation du CTR grâce aux étoiles et au prix affichés directement dans les SERP. Sur 15 000 fiches, cet écart de CTR représente environ 8 000 clics organiques supplémentaires par mois.
Product Schema et pages connexes : catégories, variantes, canonical
Variantes produit : ProductGroup vs Product dupliqué
Un produit vendu en 4 couleurs avec 4 URLs distinctes pose un problème classique : faut-il un balisage Product par variante, ou un balisage unique sur une page canonique ?
Depuis 2024, Google supporte le type ProductGroup (en bêta) qui permet de déclarer un produit parent avec ses variantes. Mais en pratique, la plupart des sites gèrent les variantes via un selector sur une même URL, avec un seul Product et des Offer qui changent dynamiquement. Dans ce cas, le JSON-LD doit refléter la variante affichée par défaut — celle que Googlebot verra au premier rendu.
Si chaque variante a sa propre URL, chaque URL doit avoir son propre balisage Product avec un sku distinct. Et chaque URL doit être auto-canonique — ne canonicalisez pas toutes les variantes vers une URL unique, sauf si elles sont réellement dupliquées (même prix, même contenu). Pour approfondir ce sujet, consultez notre guide sur les canonical URLs.
Pages catégories et ItemList
Vous ne devez pas mettre de Product complet sur les pages catégories. En revanche, un balisage ItemList référençant les produits est accepté :
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Casques audio sans fil",
"numberOfItems": 24,
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"url": "https://www.audiostore.fr/produits/prosound-x7"
},
{
"@type": "ListItem",
"position": 2,
"url": "https://www.audiostore.fr/produits/bassking-pro"
}
]
}
Ce balisage ne déclenche pas de rich result produit sur la page catégorie, mais il aide Google à comprendre la structure hiérarchique de votre catalogue et la relation entre les pages listing et les fiches produit. Sur un site de 15 000+ produits avec des paginations complexes, cette clarification structurelle aide au crawl budget.
Produits en rupture définitive
Un produit retiré du catalogue ne doit pas conserver un balisage Product avec availability: InStock. Deux options :
- Rupture temporaire : changez
availabilityenOutOfStock, conservez la page et le balisage. Google affiche "En rupture de stock" dans le rich result. - Rupture définitive : si vous supprimez la page, redirigez en 301 vers la catégorie parente ou un produit équivalent. Si vous conservez la page (pour garder les backlinks), retirez le bloc
Offerdu JSON-LD et ajoutez unnoindexsi le produit n'a aucune valeur SEO résiduelle. Plus de détails sur la gestion des redirections dans le contexte d'une migration.
Monitoring continu : ne pas perdre ses rich results
Les rich results produit sont volatils. Un déploiement qui casse le template JSON-LD, une mise à jour de prix qui introduit un format invalide, un plugin CMS qui se met à jour et ajoute un second bloc — ces régressions arrivent sans prévenir et peuvent affecter des milliers de pages en quelques heures.
Le rapport Product de Search Console n'est mis à jour qu'avec un délai de 2 à 5 jours. Si votre dernier déploiement a supprimé le JSON-LD sur 8 000 fiches vendredi, vous ne le verrez pas avant mercredi. C'est exactement le type de régression que Seogard détecte en temps réel : une disparition massive de balisage structuré déclenche une alerte avant que l'impact SEO ne devienne visible dans Search Console.
Pour un monitoring DIY, vous pouvez aussi scripter une vérification via l'URL Inspection API sur un échantillon de pages après chaque déploiement.
Côté tests automatisés dans votre CI/CD, ajoutez une assertion qui vérifie la présence du JSON-LD Product dans le HTML rendu de vos pages critiques :
# Script de smoke test post-déploiement
#!/bin/bash
URLS=(
"https://www.audiostore.fr/produits/prosound-x7"
"https://www.audiostore.fr/produits/bassking-pro"
"https://www.audiostore.fr/produits/clearvoice-m3"
)
ERRORS=0
for url in "${URLS[@]}"; do
JSONLD_COUNT=$(curl -s "$url" | grep -c 'application/ld+json')
PRODUCT_PRESENT=$(curl -s "$url" | grep -o '"@type":"Product"' | head -1)
if [ -z "$PRODUCT_PRESENT" ]; then
echo "FAIL: Product JSON-LD manquant sur $url"
ERRORS=$((ERRORS + 1))
else
echo "OK: $url ($JSONLD_COUNT blocs JSON-LD détectés)"
fi
done
if [ $ERRORS -gt 0 ]; then
echo "ALERTE: $ERRORS pages sans Product Schema détectées"
exit 1
fi
Intégrez ce script dans votre pipeline CI (GitHub Actions, GitLab CI, Jenkins) pour bloquer un déploiement qui casserait le balisage structuré sur vos pages à plus fort trafic.
Au-delà du balisage : ce que Google fait réellement avec vos données produit
Un Product Schema correct ne garantit pas un rich result. Google applique des critères de qualité non documentés : ancienneté du domaine dans le Google Merchant Center, historique de conformité des données structurées, cohérence prix/contenu visible.
Les signaux les plus impactants, par ordre de priorité observé :
- Conformité parfaite du balisage (pas d'erreurs, pas d'avertissements)
- Cohérence prix JSON-LD / prix visible (Google fait un visual match)
- Volume et fraîcheur des avis (un produit avec 2 avis de 2021 est moins éligible qu'un produit avec 150 avis récents)
- Présence dans le Google Merchant Center (même gratuit, le feed Merchant renforce la confiance de Google dans vos données produit)
- Performance de la page (une fiche produit avec un LCP au-delà de 4s peut voir son éligibilité réduite)
Le Product Schema est une base technique nécessaire mais pas suffisante. C'est l'hygiène structurelle qui rend vos produits lisibles par Google — le déclenchement effectif des rich results dépend d'un écosystème plus large. Mettez le balisage en place, surveillez sa cohérence en continu, et laissez le reste suivre.