Table des matières :
- Périmètre : Product / Review / Breadcrumb et contraintes réelles côté Google
- Implémenter schema.org/Product en JSON-LD (PrestaShop 8.1.x / PHP 8.2+)
- Reviews et AggregateRating : baliser uniquement ce qui est visible (sinon rejet)
- BreadcrumbList : stabiliser le fil d’Ariane et aligner avec la canonical
- Maillage interne e-commerce : ce que vous pouvez automatiser sans casser le crawl
- Validation, monitoring et garde-fous (Rich Results, GSC, perf)
Périmètre : Product / Review / Breadcrumb et contraintes réelles côté Google
Sur un site e-commerce, les trois balisages structurés qui ont un ROI technique immédiat sont généralement schema.org/Product, schema.org/Review (et AggregateRating) et schema.org/BreadcrumbList. Ils ne “font” pas le SEO à eux seuls, mais ils réduisent l’ambiguïté sémantique pour les moteurs et conditionnent l’éligibilité à certains affichages (rich results). Google le rappelle sans détour : “Structured data is a standardized format for providing information about a page and classifying the page content.” (Google Search Central, documentation « Structured data » : Documentation Structured data — Google Search Central).
Deux nuances importantes (souvent sources de malentendus en production) :
- Validité schema.org ≠ éligibilité Google : vous pouvez avoir un JSON-LD “valide” (au sens schema.org) et pourtant ne pas déclencher d’affichage enrichi, parce que Google applique des règles de qualité, de cohérence page/markup, et des contraintes spécifiques par type de rich result.
- Le schéma doit refléter ce que l’utilisateur voit : dès que le balisage diverge du rendu (prix, stock, note, devise, disponibilité), vous créez un signal contradictoire. Sur e-commerce, c’est typiquement là que la confiance “algorithmique” se dégrade.
Côté PrestaShop, il faut partir d’un constat : le cœur ne garantit pas un JSON-LD complet et cohérent sur toutes les pages et tous les thèmes. Selon votre stack (PrestaShop 8.1.x Classic en Smarty, ou PrestaShop 9 avec Symfony 6.4 côté back et un front qui reste majoritairement en templates), vous aurez au mieux des variables exploitables via le presenter produit, mais pas un schéma “prêt pour Google” (notamment sur la gestion des déclinaisons, des prix, de la disponibilité, et des avis).
Ajoutez à ça des contraintes “terrain” très fréquentes en France / UE :
- Affichage TTC vs HT (B2C vs B2B, groupe client, TVA intracom) : votre JSON-LD doit reprendre le prix effectivement affiché pour l’utilisateur, sinon vous créez une incohérence détectable.
- Multi-pays / multi-devises (France/Belgique/Suisse par exemple) : l’URL canonique, la devise (
priceCurrency) et parfois la disponibilité varient selon boutique/pays. Le schéma doit suivre le contexte (store, devise, géolocalisation) dans lequel la page est rendue.
Pour la référence “source” des propriétés, schema.org documente l’objet Product ici : schema.org/Product (utile pour comprendre ce qui est standard, même si Google n’exploite pas tout).
Pré-requis et risques (à traiter comme du code prod) : travaillez sous Git, sur un staging, avec un plan de rollback. Toute injection JSON-LD via template ou module peut introduire des régressions (erreurs d’échappement, duplications de balises, incohérences prix/stock). Avant déploiement, faites un contrôle qualité type audit technique ; la checklist de publication décrite dans l’article Audit SEO technique : contrôle qualité avant publication des pages web est un bon point de départ.
Implémenter schema.org/Product en JSON-LD (PrestaShop 8.1.x / PHP 8.2+)
Le schéma Product utilisé par Google repose sur une séparation nette : l’entité Product (nom, images, marque, identifiants) et le bloc offers (prix, devise, disponibilité, URL). Techniquement, l’erreur la plus fréquente sur PrestaShop est de sérialiser un produit “théorique” alors que l’HTML affiche un prix de déclinaison ou une disponibilité dépendante du stock, du groupe client, ou du pays. Si le JSON-LD dit “InStock” mais que votre bouton “Ajouter au panier” est désactivé, vous vous exposez à une invalidation (ou à minima une perte de confiance).
Deux approches propres :
1) Thème (quick win) : injecter un <script type="application/ld+json"> dans themes/VOTRE_THEME/templates/catalog/product.tpl en exploitant les variables déjà normalisées ($product, $currency, $urls). C’est rapide mais plus fragile lors des mises à jour de thème.
2) Module (durable) : un module “SEO schema” qui hooke displayHeader et ne rend le JSON-LD que sur product / category selon le contrôleur. C’est plus maintenable et testable, et ça évite les merges Smarty à chaque patch.
Avant même d’écrire une ligne, clarifiez votre “source de vérité” sur 4 champs sensibles :
- URL : utilisez la canonique (pas l’URL avec tracking / paramètres).
- Prix : prix final affiché (ex. promo incluse), dans la bonne unité (souvent TTC en B2C France).
- Disponibilité : basée sur vos règles de commande (stock réel, backorders, “disponible à la commande”, etc.).
- Identifiants :
sku,gtin13/EAN,mpnsi pertinent — et surtout ne pas pousser des champs vides (mieux vaut omettre que remplir avec"").
Petit tableau pratique (mapping “utile” côté PrestaShop, à adapter selon votre thème/modules) :
| Besoin JSON-LD | Propriété | Variable PrestaShop fréquente | Remarque |
|---|---|---|---|
| Nom produit | name |
$product.name |
Doit correspondre au H1 |
| Canonical | url (dans offers) |
$product.canonical_url |
Éviter les URLs paramétrées |
| Image principale | image |
$product.cover...url |
Ajoutez plusieurs images si dispo |
| Référence | sku |
$product.reference |
Omettez si non utilisé |
| EAN | gtin13 |
$product.ean13 |
Très courant en FR (EAN-13) |
| Marque | brand.name |
$product.manufacturer_name |
Omettez si inexistante |
| Prix | offers.price |
$product.price_amount |
Vérifier TTC/HT, promo incluse |
| Stock | offers.availability |
$product.quantity (souvent) |
Attention ASM/backorders |
Exemple minimaliste côté template (testé conceptuellement pour PrestaShop 8.1.x Classic ; à adapter selon vos variables réelles). Point important : utilisez l’URL canonique, pas l’URL courante avec paramètres.
{* themes/VOTRE_THEME/templates/catalog/product.tpl *}
{block name='head_seo_schema'}
{$schemaProduct = [
'@context' => 'https://schema.org',
'@type' => 'Product',
'@id' => ($product.canonical_url|escape:'htmlall':'UTF-8')|cat:'#product',
'name' => $product.name,
'description' => $product.description_short|strip_tags,
'image' => [$product.cover.bySize.large_default.url],
'sku' => $product.reference,
'gtin13' => $product.ean13,
'brand' => ['@type' => 'Brand', 'name' => $product.manufacturer_name],
'offers' => [
'@type' => 'Offer',
'url' => $product.canonical_url,
'priceCurrency' => $currency.iso_code,
'price' => $product.price_amount,
'availability' => ($product.quantity > 0) ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock'
]
]}
<script type="application/ld+json">
{$schemaProduct|@json_encode:JSON_UNESCAPED_UNICODE|@replace:'\/':'/' nofilter}
</script>
{/block}
Deux améliorations “production-grade” à envisager dès que vous sortez du POC :
- Nettoyage des champs : si
gtin13oubrand.nameest vide, supprimez la clé plutôt que de l’envoyer vide. Idem poursku. C’est un détail, mais ça réduit les avertissements et les signaux incohérents. - Images multiples : si votre fiche produit affiche 4 visuels, envoyez-les (dans l’ordre). Google peut s’en servir pour enrichir l’affichage et vous évitez un “markup minimal” trop pauvre.
Points durs à traiter proprement sur un vrai catalogue :
- Déclinaisons : si vos prix/stock changent par combinaison, vous devez soit (a) baliser le produit par défaut affiché (combinaison sélectionnée), soit (b) utiliser
AggregateOfferaveclowPrice/highPrice(mais ça demande de calculer un range cohérent). Ne poussez pas un range “à la louche”.
Mini-scenario réaliste : un t-shirt vendu en 8 tailles, où seules 3 tailles sont en stock. Si vous publiezInStockau niveau produit sans nuance, alors qu’une taille “par défaut” est en rupture, vous pouvez afficher un signal contradictoire dès l’arrivée depuis Google. Dans ce cas, soit vous forcez la taille par défaut réellement commandable, soit vous passez sur un balisage plus prudent (ou “OutOfStock” si c’est la réalité de la sélection initiale). - Devise / taxes : assurez-vous que
price_amountest bien celui affiché (TTC/HT selon votre front). Google s’attend à un prix cohérent avec le visuel. En pratique, sur des boutiques FR, le point de friction est souvent l’affichage TTC en front mais une variable de prix récupérée HT via un mauvais contexte. - URL : si vous avez des règles de réécriture ou des routes custom, référez-vous à Génération d’URL PrestaShop : Link, routes Symfony et legacy link pour éviter les URL “hors canon”.
Enfin, gardez un principe simple : un seul Product JSON-LD par page produit. Si votre thème en rend un, désactivez celui du module (ou inversement), sinon vous créez des doublons et des divergences (prix/stock, images, etc.).
Reviews et AggregateRating : baliser uniquement ce qui est visible (sinon rejet)
Le schéma Review est celui qui se fait le plus souvent recaler, parce que beaucoup de sites génèrent un aggregateRating alors que la note n’est pas réellement affichée, ou parce que les avis proviennent d’un agrégateur non contrôlé. Google est explicite : “Review snippets are short excerpts of a review or a rating from a review website.” (Google Search Central, « Review snippet » : Review snippet — Google Search Central). Traduction côté implémentation : si votre page produit n’affiche pas la note et le nombre d’avis, ne marquez rien.
Concrètement, pour rester “safe” (et éviter le yo-yo d’éligibilité) validez ces conditions avant d’émettre aggregateRating :
- La note moyenne est visible sans action complexe (ex. pas uniquement après login).
- Le nombre d’avis est visible et cohérent.
- Les avis sont liés au produit présent sur la page (pas à une catégorie, pas à la boutique entière).
- Vous ne “fabriquez” pas une note en agrégeant des sources non affichées.
Sur PrestaShop, les avis viennent souvent d’un module (ex. productcomments, ps_customerreviews, ou un SaaS). Le cœur n’expose pas une API uniforme. La stratégie la moins mauvaise : récupérer la même donnée que celle rendue dans le template d’avis, et la sérialiser dans le même endroit (même contrôleur, même contexte). Sinon vous allez tôt ou tard produire une divergence (cache, filtrage par boutique, modération, avis non publiés, etc.).
Pattern “propre” en module (pseudo-code, à adapter selon le module d’avis réellement installé) :
- Hook
displayHeaderoudisplayFooterProduct. - Vérifier qu’on est bien sur une page produit (
$this->context->controller->php_self === 'product'). - Récupérer (a)
ratingValue, (b)reviewCount, et éventuellement (c) les 3 derniers avis publiés. - Ajouter
aggregateRating+review[]uniquement si la note est visible et le comptage > 0.
Exemple de fragment JSON-LD à fusionner dans votre Product (ne dupliquez pas un second @context, fusionnez les champs dans le même objet Product) :
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": 4.6,
"reviewCount": 127
},
"review": [
{
"@type": "Review",
"author": {"@type": "Person", "name": "Client vérifié"},
"datePublished": "2026-06-30",
"reviewBody": "Bon produit, livraison rapide.",
"reviewRating": {"@type": "Rating", "ratingValue": 5, "bestRating": 5}
}
]
Trois pièges concrets en e-commerce PrestaShop :
1) Avis non visibles : si vous masquez les avis derrière un onglet chargé en JS, assurez-vous qu’ils sont bien rendus côté serveur (ou au moins indexables) ; sinon, vous marquez du contenu non visible.
Exemple courant : l’onglet “Avis” est vide au chargement, puis une requête AJAX injecte le HTML. Dans ce cas, vous pouvez : (a) rendre le résumé (note + count) côté serveur et ne baliser que aggregateRating, ou (b) rendre un extrait d’avis côté serveur (3 derniers) et charger le reste ensuite.
2) Pages non produit : ne mettez pas Review sur une catégorie, une home, ou une landing “Top ventes”. Google refuse souvent les review snippets quand l’entité évaluée n’est pas claire.
3) Modération : si vous modérez, ne comptez que les avis publiés. Les modules d’avis stockent souvent des statuts ; la donnée “source de vérité” n’est pas le total en base, c’est le total publié. Pour éviter les erreurs de perf en prod, profilez vos requêtes (N+1 sur les avis = classique) avec PrestaShop debug profiling : activer et analyser performances SQL.
Un garde-fou simple : si votre module d’avis est instable (ou trop coûteux en SQL), commencez par déployer Product + Breadcrumb, puis ajoutez Reviews dans un second lot, après optimisation. Mieux vaut un schéma incomplet mais fiable qu’un schéma “riche” qui casse le TTFB ou diverge du front.
BreadcrumbList : stabiliser le fil d’Ariane et aligner avec la canonical
Le balisage BreadcrumbList est souvent sous-estimé, alors que c’est celui qui vous aide à stabiliser l’interprétation “catégorie > produit”, surtout quand PrestaShop reconstruit le breadcrumb selon la navigation (catégorie précédente, module, ou paramètre). Google le formule clairement : “Breadcrumb structured data helps Google understand the structure of your site, and can be used to display breadcrumbs in search results.” (Google Search Central, « Breadcrumb » : Breadcrumb — Google Search Central).
Dans PrestaShop, un problème fréquent est la multiplicité des chemins vers un produit : catégories multiples, filtres à facettes, liens depuis recherche interne, campagnes, etc. Si votre breadcrumb HTML varie, votre BreadcrumbList variera aussi, et vous vous retrouvez avec des signaux contradictoires. Le correctif le plus robuste consiste à choisir un chemin “référence” (souvent via id_category_default du produit, ou une logique métier) et à générer le breadcrumb indépendamment de la navigation.
En pratique, posez-vous une question “architecture” : quelle est la catégorie principale qui doit porter le produit dans l’arborescence ?
Sur un e-commerce FR typique, vous voulez éviter le cas “Produit accessible via 6 catégories, et Google choisit un chemin inattendu”. Stabiliser le fil d’Ariane (HTML + JSON-LD) revient à stabiliser ce choix.
Implémentation côté thème (Classic) : vous avez généralement accès à $breadcrumb.links (liste ordonnée de liens). L’idée est de sérialiser ces liens en itemListElement avec un position stable et des URL absolues.
{* themes/VOTRE_THEME/templates/_partials/breadcrumb.tpl (ou override) *}
{assign var=items value=[]}
{foreach from=$breadcrumb.links item=link name=bc}
{append var=items value=[
'@type' => 'ListItem',
'position' => $smarty.foreach.bc.iteration,
'name' => $link.title,
'item' => $link.url
]}
{/foreach}
<script type="application/ld+json">
{[
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items
]|@json_encode:JSON_UNESCAPED_UNICODE|@replace:'\/':'/' nofilter}
</script>
Recommandations pragmatiques :
- N’incluez pas de niveaux artificiels (ex. “Accueil > Recherche > Produit”), sauf si ces pages existent et sont réellement dans votre architecture.
- Alignez breadcrumb et canonique : si votre produit a un canonique sans catégorie (cas de certains paramétrages), évitez un breadcrumb qui prétend le contraire.
- Liens absolus et localisés : en multi-langue/multi-boutique, chaque item doit pointer vers la bonne version (FR/EN, .fr/.be, etc.). Une URL “hybride” dans le breadcrumb est un classique sur les boutiques multi-stores.
- Si vous customisez la construction des liens (ex. catégorie primaire), basez-vous sur les mécanismes d’URL de PrestaShop (Link / routes) documentés dans l’article interne déjà cité sur la génération d’URL, pour éviter les URL incohérentes.
Maillage interne e-commerce : ce que vous pouvez automatiser sans casser le crawl
Le maillage interne n’est pas un slogan : c’est un problème de graphe (répartition de popularité interne), de profondeur de crawl, et de désorphanisation (pages produit non atteignables depuis la navigation). Sur PrestaShop, le maillage “par défaut” est souvent insuffisant sur les gros catalogues : catégories trop plates, pagination peu exploitable, filtres à facettes qui génèrent des combinaisons d’URL, et blocs “produits associés” qui répètent toujours les mêmes liens.
Trois leviers qui marchent en pratique, sans tomber dans l’usine à gaz :
1) Hubs de catégories : sur chaque catégorie N, exposez des liens éditorialisés vers N+1 (sous-catégories) + vers des pages “marques” ou “guides” pertinentes. Le but est de réduire la profondeur moyenne des produits (ex. viser ≤ 3 clics depuis la home pour vos top SKU).
Astuce opérationnelle : dans une catégorie “Chaussures de sécurité”, un bloc “Choisir sa norme (S1P, S3…)” qui mène vers des sous-catégories ou guides permet à la fois (a) d’aider l’utilisateur et (b) d’orienter le crawl vers des pages structurelles, pas vers des combinaisons de filtres infinies.
2) Liens contextuels depuis les fiches produit : au lieu de “Produits similaires” calculés au hasard, basez le cross-linking sur des dimensions stables : même catégorie parente, même marque, même attribut clé (ex. matériau). Si vous avez déjà optimisé la recherche interne, réutilisez ses signaux (termes fréquents, facettes) ; l’article Recherche interne PrestaShop : optimiser suggestions live et tolérance aux fautes donne une base pour structurer ces dimensions.
Mini-scenario : un produit “Batterie 18V” peut pousser “Chargeur 18V compatible” et “Adaptateur X” (accessoires), plutôt qu’un carrousel “similaire” purement catégoriel. Résultat : vous renforcez des clusters logiques (compatibilité/usage), souvent plus proches des intentions de recherche.
3) Contrôle des URL générées par la navigation : les filtres à facettes peuvent créer des milliers d’URL crawlables qui diluent le budget et le PageRank interne. Le maillage interne “propre” consiste à ne lier que vers les pages que vous assumez comme indexables (catégories, marques, pages conseils, listings triés stables). Pour les autres, utilisez canonicals, noindex, ou bloquez à la source (selon votre stratégie et vos contraintes). Si vous avez déjà mis en place un durcissement anti-abus autour des URL de facettes, vous savez pourquoi ça compte.
Pour décider quoi automatiser, une grille simple (qui évite de “sur-lier” partout) :
| Automatisation | Gain SEO attendu | Risque principal | Garde-fou |
|---|---|---|---|
| “Produits complémentaires” (accessoires) | Fort (maillage transactionnel) | Répétition / liens sitewide | Limiter à 4–8 liens, varier par catégorie |
| “Dans la même marque” | Moyen | Sur-représenter des marques dominantes | Appliquer un quota, ou random contrôlé |
| “Guides d’achat” depuis catégories | Fort (hub éditorial) | Contenu faible si pages guides pauvres | Travailler le contenu avant de pousser les liens |
| “Top ventes” sitewide | Faible à moyen | Dilution + mêmes liens partout | Déporter sur hubs de catégories |
Un pattern d’implémentation simple pour créer des liens internes “éditables” :
- Ajoutez un champ back-office (via un module) sur catégorie/produit pour stocker une liste d’IDs (catégories “à pousser”, produits “à pousser”).
- Au rendu, chargez ces entités via les presenters existants (évitez de recoder la sérialisation produit), puis affichez un bloc de liens avec ancre descriptive.
- Mettez un cache applicatif (Redis ou cache PrestaShop) sur ce bloc si vous avez beaucoup de trafic ; sinon, vous payez ces requêtes sur chaque page. Pour la stratégie cache serveur, voir Cache PrestaShop : Varnish, Redis, Memcached et OPcache côté serveur.
Un dernier point, souvent oublié : le maillage interne doit aussi respecter la cohérence URL/canonical. Si vous créez des liens internes vers des URLs avec paramètres (tri, pagination atypique, facettes), vous “fabriquez” un crawl inutile. D’où l’intérêt d’aligner vos liens internes avec votre stratégie d’URL (et de relire au besoin votre implémentation de routes et de canonicalisation via l’article sur la génération d’URL PrestaShop déjà cité).
Validation, monitoring et garde-fous (Rich Results, GSC, perf)
Une implémentation schema + maillage interne se valide comme une feature : tests + monitoring + rollback. Pour le schéma, utilisez :
Le premier est votre “oracle” pour l’éligibilité Google, le second est utile pour vérifier que votre JSON-LD est valide schema.org même si Google n’en fait rien.
Avant mise en prod, un protocole de test simple (et réaliste) sur un e-commerce :
- Tester 5 fiches produits représentatives : produit en stock, produit en rupture, produit avec promo, produit avec déclinaisons, produit sans marque/EAN.
- Tester 2 catégories (si vous injectez du breadcrumb via template global).
- Vérifier qu’il n’y a qu’un seul script
application/ld+json“Product” par page (et qu’il n’est pas dupliqué par un module tiers).
En production, surveillez trois signaux concrets :
- Dans Google Search Console : rapport “Améliorations” (Products, Breadcrumbs, Reviews si éligible) et taux d’URL valides vs valides avec avertissements. Pensez aussi à contrôler des URLs précises via l’inspection d’URL quand vous venez de déployer un correctif sur le prix/stock.
- Dans les logs applicatifs : erreurs Smarty / exceptions module lors du rendu (un JSON mal échappé peut casser le HTML). Mettez une vraie collecte d’erreurs ; l’article PrestaShop monitoring d’erreurs : logs PHP, MySQL, JavaScript et alertes e-mail couvre une approche exploitable.
- Côté performance : un bloc de JSON-LD ne doit pas déclencher de requêtes supplémentaires non cachées. Si votre module d’avis fait 5 requêtes par produit, vous venez de vous auto-saboter. Mesurez le TTFB et le profil SQL ; en cas de dégradation, revenez à un rendu minimal (Product sans avis) le temps d’optimiser. Pour une cible TTFB réaliste sur boutique optimisée, voir Réduire le TTFB PrestaShop sous 200 ms.
Enfin, gardez une règle d’hygiène : pas de duplication. Un même produit ne doit pas être balisé deux fois (ex. un module + un thème + un script externe). Centralisez : soit thème, soit module, pas les deux. Et à chaque mise à jour PrestaShop/module/thème, repassez la batterie de tests (rich results + crawl interne) avant de pousser en prod — exactement comme vous le feriez après un changement de cache ou de performance (sur ce point, le diagnostic via profiling SQL est souvent votre meilleur “capteur” : activer le debug profiling et lire les requêtes).
