Table des matières :
- Merchant Center : pourquoi la synchronisation PrestaShop finit (souvent) en produits refusés
- Flux fichier vs Content API : choisir le mécanisme de sync sans se tromper d’outillage
- Mapping PrestaShop → attributs Merchant Center : définitions, identifiants stables et multi-boutiques
- Génération automatique du flux : module, commande CLI et cron (sans tuer le serveur)
- Déclinaisons, prix, taxes, promos et stock : les pièges qui cassent la conformité
- Performance, observabilité et sécurité : industrialiser la sync au lieu de la subir
Merchant Center : pourquoi la synchronisation PrestaShop finit (souvent) en produits refusés
Sur le papier, « synchroniser PrestaShop avec Google Merchant Center » se résume à pousser un flux produit. En production, c’est surtout un problème de qualité de données et de fraîcheur. Merchant Center sanctionne rapidement les incohérences les plus courantes : prix sur la page ≠ prix envoyé, disponibilité fausse, URL cassées, images non accessibles, attributs obligatoires manquants (GTIN, marque, condition), ou encore pays/devise non alignés. La conséquence est triviale : item disapproved ou limited performance et vous perdez la couverture Shopping / Performance Max sur une partie du catalogue.
Le point le plus sous-estimé est la dérive entre ce que calcule PrestaShop (prix TTC, prix spécifique, taxe selon pays, arrondis, stock avancé, mode “commande possible hors stock”) et ce que vous exportez. Un export « naïf » qui lit ps_product.price et ps_stock_available.quantity ne reflète pas le front. Google insiste sur l’exactitude et la mise à jour des données dans sa spécification produit (Google Merchant Center Help – Product data specification : spécification produit Google Merchant Center). Autrement dit : si votre flux n’est pas aligné sur le rendu et les règles de prix, ce n’est pas un “petit warning”, c’est une instabilité structurelle.
Un exemple très concret (fréquent en France/UE) :
- votre boutique affiche un prix TTC qui dépend du pays (TVA) + d’une règle de prix spécifique,
- votre flux exporte un prix “HT” (ou TTC mais avec une TVA par défaut),
- Google explore la page et détecte un écart (même quelques centimes si l’arrondi diffère).
Résultat : refus pour incohérence de prix (et parfois propagation à des produits “similaires” si le problème est systémique).
Pour réduire ces refus, vous gagnez du temps si vous adoptez une routine de débogage “factuelle” :
- Choisir 5 SKUs représentatifs (promo, variante, hors stock commandable, multi-TVA, produit avec déclinaisons).
- Pour chacun : comparer front vs flux sur
price,availability,link,image_link,gtin/brand. - Contrôler l’accessibilité : landing page et images doivent répondre en 200, en HTTPS, sans redirection infinie, et sans blocage de Googlebot (robots, WAF, protection hotlink).
- S’assurer que l’URL envoyée correspond bien à la canonique (sinon vous créez des écarts de contenu/prix entre URLs).
Enfin, la synchronisation n’est pas qu’un sujet applicatif, c’est un sujet d’infra. Un gros export (10k–200k SKU) déclenché en cron peut faire exploser le CPU PHP-FPM, saturer MySQL et vous mener à des 503 pendant les pics (si vous n’avez pas de garde-fous). Pour la partie diagnostic, gardez sous la main vos runbooks côté serveur et logs, typiquement le guide sur les erreurs 503 : Erreur HTTP 503 : diagnostic serveur, logs et ressources.
Flux fichier vs Content API : choisir le mécanisme de sync sans se tromper d’outillage
Deux familles existent : flux fichier (XML/CSV/TSV) déposé (HTTP(S), SFTP, Google Sheets) et Content API for Shopping. Le flux fichier est robuste et simple à opérer (un fichier atomique + une URL), mais il est souvent « batch only » : vous publiez un snapshot du catalogue, pas des deltas à la minute. L’API est plus adaptée si vous avez des prix/stocks très volatils ou un catalogue segmenté par canaux, mais elle vous impose l’auth OAuth2, la gestion des quotas, des retries et une observabilité propre (Content API for Shopping (docs)).
En pratique, le bon choix dépend moins de la “modernité” que de vos contraintes :
| Critère | Flux fichier | Content API |
|---|---|---|
| Simplicité d’exploitation | ✅ très bonne | ❌ plus complexe (OAuth, quotas) |
| Mises à jour fréquentes (prix/stock volatils) | ⚠️ possible mais batch | ✅ très adaptée (updates ciblés) |
| Résilience (rejouer, atomique) | ✅ facile (tmp + rename) | ⚠️ nécessite stratégie de retries/idempotence |
| Observabilité | ⚠️ à construire | ⚠️ à construire (mais plus granulaire) |
| Time-to-market | ✅ rapide | ❌ plus long |
Dans PrestaShop (testé sur PrestaShop 8.1.x et 9.x, PHP 8.2), l’erreur classique est de démarrer avec l’API “par principe” et de finir avec un code fragile (timeouts, quota, doublons d’ID). Pour un premier niveau industriel : flux TSV/XML généré côté shop + récupération planifiée par Google est souvent le meilleur ratio complexité/fiabilité.
Mini-scenario “réaliste” : un marchand multi-boutique FR/BE exporte 60k produits. Les prix changent 2 fois/jour (promos), le stock 10 fois/jour (ERP). Dans ce cas :
- un flux complet quotidien sécurise la réconciliation,
- un flux incrémental (toutes les 2–4 heures) limite la fenêtre d’incohérence,
- et l’API ne devient nécessaire que si vous devez pousser des updates quasi temps réel (ex. ruptures critiques), ou si vous pilotez des champs annexes (shipping services, règles complexes) de façon très dynamique.
Si vous devez passer en API, faites-le avec une vraie file (queue) ou un mécanisme d’outbox interne, pas un script PHP qui boucle 50 000 updates dans un cron.
Mapping PrestaShop → attributs Merchant Center : définitions, identifiants stables et multi-boutiques
Le mapping ne doit pas partir de la base, mais du contrat Google : un produit Merchant Center est une ressource avec un id stable, un title, une description, un link, un image_link, un price, une availability, un brand et idéalement un gtin (ou mpn si vous n’avez pas de GTIN). La spec officielle est ici : spécification produit Google Merchant Center. La règle opérationnelle : si vous ne pouvez pas garantir stabilité + cohérence de ces champs, vous ne “synchronisez” pas, vous “poussez du bruit”.
Côté PrestaShop, l’id Merchant Center doit être dérivé d’un identifiant immuable. En pratique : product-{id_product} pour un produit simple, et product-{id_product}-{id_product_attribute} pour une déclinaison (si vous exportez les variantes). Ne réutilisez pas le reference comme clé primaire : c’est éditable, parfois vide, et en multi-boutique ça devient vite incohérent. Le link doit être une URL canonique front, générée via Link (et non une concat bricolée), ce qui rejoint les problématiques de génération d’URL : Génération d’URL PrestaShop : Link, routes Symfony et legacylink.
Pour rendre le mapping “audit-able”, une table de correspondance simple aide beaucoup (à adapter à votre modèle, variantes, et multi-shop) :
| Attribut Merchant Center | Source PrestaShop (idée) | Notes d’alignement |
|---|---|---|
id |
id_product + id_product_attribute |
Stable, jamais recalculé à partir d’un champ éditable |
title |
product_lang.name (+ déclinaison si export variantes) |
Éviter les titres trop courts / trop génériques |
description |
product_lang.description_short (ou description) |
Nettoyer HTML si nécessaire, rester fidèle au front |
link |
Link::getProductLink() |
Contexte boutique/langue, URL canonique |
image_link |
Link::getImageLink() |
Tester accessibilité publique + HTTPS |
price |
Calcul “front” (prix final TTC) | Ne pas lire directement ps_product.price |
sale_price |
Prix promo “effectif” | Cohérence Omnibus si affichage de réductions |
availability |
Logique disponibilité (stock + backorders + date) | Ne pas réduire à quantity > 0 |
brand |
manufacturer.name |
Obligatoire selon catégories |
gtin |
EAN/UPC (souvent product.ean13) |
Très important en FR/UE pour l’approbation et le matching |
mpn |
reference ou champ dédié |
Seulement si pas de GTIN (à documenter) |
Le multi-langue et le multi-boutique sont des multiplicateurs d’emmerdes. Merchant Center segmente par pays/langue (feeds, shipping, taxes). Dans PrestaShop, vous devez figer un contexte (idshop, idlang, idcurrency, idcountry) au moment de calculer prix/URL.
Deux règles pratiques (souvent négligées) :
- Un flux = un contexte : si vous mixez des prix EUR France et des URLs d’une autre boutique, vous créez des incohérences impossibles à diagnostiquer.
- Ne recodez pas la logique de prix : appuyez-vous sur les classes PrestaShop en explicitant le contexte, et testez avec un catalogue incluant taxes, prix spécifiques, arrondis, et produits “out of stock”. (Si vous devez approfondir côté API/architecture PrestaShop, la documentation officielle PrestaShop DevDocs est une base stable : PrestaShop DevDocs)
Génération automatique du flux : module, commande CLI et cron (sans tuer le serveur)
L’approche maintenable consiste à encapsuler l’export dans un module qui expose une commande CLI Symfony (PrestaShop 9 est nativement sur Symfony 6.4 ; PrestaShop 8.1 utilise aussi des briques Symfony côté BO). La commande permet d’exécuter l’export hors requête HTTP, avec des limites de mémoire maîtrisées, et un vrai code de retour. Pour la base, référez-vous à la structure de module moderne : Module PrestaShop 9 : structure, services et bonnes pratiques Symfony et aux commandes disponibles/bonnes pratiques : Commandes CLI PrestaShop : liste, catégories et options d’aide.
Un squelette minimal (pseudo-code) ressemble à ça :
// PrestaShop 8.1/9.x, PHP 8.2
#[AsCommand(name: 'merchant:feed:export')]
final class ExportMerchantFeedCommand extends Command
{
public function __construct(
private readonly FeedWriter $writer,
private readonly ProductProvider $provider,
private readonly LockFactory $lockFactory,
) { parent::__construct(); }
protected function execute(InputInterface $input, OutputInterface $output): int
{
$lock = $this->lockFactory->createLock('merchant_feed_export', 3600);
if (!$lock->acquire()) {
$output->writeln('Export already running');
return Command::FAILURE;
}
$since = new \DateTimeImmutable($input->getOption('since') ?? '-24 hours');
foreach ($this->provider->iterateChangedProducts($since, 500) as $batch) {
$this->writer->append($batch); // TSV/XML streaming
}
$this->writer->finalizeAtomic(); // write tmp + rename
return Command::SUCCESS;
}
}
Les points non négociables : lock pour éviter deux crons simultanés, écriture stream (pas un tableau géant en mémoire), et publication atomique (fichier temporaire puis rename). Une bonne pratique opérationnelle consiste aussi à séparer la génération et la serving layer :
- générer le fichier dans un répertoire non public, puis le déplacer vers un répertoire servi en statique (Nginx/Apache) ;
- éviter de servir le feed via PHP (moins robuste, plus coûteux).
Pour rendre le flux exploitable dès le premier incident, ajoutez deux éléments simples :
- Un en-tête stable et conforme (si TSV) et des champs strictement typés (prix, devise, disponibilité).
- Un “mode validation” (option CLI) qui sort 20 lignes et s’arrête, pour inspection rapide et tests CI.
Exemple minimal de lignes TSV (illustratif) :
id title description link image_link price availability brand gtin condition
product-42-0 Chaussure running Homme X Chaussure légère... https://example.fr/chaussures/42-x.html https://example.fr/1234-large_default/x.jpg 89.90 EUR in_stock MarqueX 1234567890123 new
En cron système, vous visez souvent un export incrémental fréquent (deltas) + un export complet quotidien pour “réconcilier”. Et vous surveillez les ressources : si l’export déclenche des 503, vous êtes en train de faire tourner un job batch sur la même infra que le front sans isolation (ou sans limites).
Déclinaisons, prix, taxes, promos et stock : les pièges qui cassent la conformité
Les déclinaisons (combinations) sont la source #1 de flux incohérents. Si vous exportez un produit parent sans variantes alors que le front force une sélection d’attribut, Google peut crawler une page qui montre un autre prix / une autre disponibilité que celle envoyée. Dans beaucoup de catalogues, la bonne stratégie est d’exporter une ligne par déclinaison quand les variantes diffèrent en prix, stock ou image. Pour la partie “comment identifier et afficher correctement les déclinaisons”, gardez ce repère : Afficher les déclinaisons en liste produits (module attributs).
Sur le prix, ne confondez jamais “prix de base” et “prix effectif”. Merchant Center attend un prix qui matche la page, typiquement TTC selon le pays ciblé. En PrestaShop, entre taxes, prix spécifiques (ps_specific_price), règles panier, groupes clients, écotaxes et arrondis, le calcul est non trivial.
Checklist “anti mismatch prix” (utile avant de blâmer Google) :
- La page produit affichée au crawler montre-t-elle le même prix TTC que votre flux ?
- Le flux est-il calculé dans le bon contexte (pays, devise, boutique) ?
- Avez-vous des modules qui modifient le prix à l’affichage (ex. prix dynamiques, scripts) sans que le back-office ne reflète la même valeur ?
- Vos promotions utilisent-elles des périodes (début/fin) cohérentes avec le
sale_priceexporté, surtout lors des changements d’heure / fuseau ?
Bonus 2026 : si vous affichez un prix barré / antérieur, la Directive Omnibus impose des contraintes d’historisation côté UE ; c’est surtout un sujet front et conformité, mais ça influence la cohérence des promos si vous exportez sale_price. Le rappel côté PrestaShop est ici : Directive Omnibus PrestaShop : afficher le prix antérieur conforme.
Sur le stock/disponibilité, Merchant Center se base sur availability (in_stock, out_of_stock, preorder, etc.). PrestaShop a plusieurs modes : stock avancé, backorders, “autoriser commande quand hors stock”, gestion par déclinaison.
Une manière pragmatique d’éviter les mensonges “involontaires” est de mapper la disponibilité sur la capacité d’achat réelle plutôt que sur la quantité brute :
- Si le produit n’est pas disponible à la commande (
available_for_order = 0) →out_of_stock(même si la quantité est > 0, cas rare mais possible selon logique métier). - Si quantité > 0 →
in_stock. - Si quantité = 0 mais commande autorisée hors stock →
in_stock(oupreordersi vous avez une date de dispo future réellement affichée au client). - Si
available_datefuture et vous acceptez des commandes →preorder(à condition que votre front annonce bien cette information, sinon vous recréez un écart).
Enfin, n’oubliez pas que la conformité ne se joue pas uniquement sur prix/stock : si vos délais ou frais de livraison réels diffèrent de ce que Merchant Center comprend (via vos réglages de livraison), vous augmentez le risque de “shipping inconsistency”. Dans un contexte FR/UE où les transporteurs et délais varient fortement (domicile, point relais, express), gardez une cohérence stricte entre :
- ce qui est annoncé au checkout,
- les paramètres Merchant Center (shipping, services),
- et les pages d’information livraison accessibles au crawler.
Performance, observabilité et sécurité : industrialiser la sync au lieu de la subir
Un export Merchant Center est un job de données ; traitez-le comme tel. Sur MySQL, minimisez les requêtes N+1 : itérez par lots avec des SELECT ciblés, indexez les colonnes utilisées pour détecter les changements (date_upd, jointures vers product_shop, specific_price, stock_available). Si vous ne mesurez pas, vous optimisez à l’aveugle : activez le slow query log et traitez les pires requêtes avant d’augmenter les ressources. Guide pratique : Requêtes MySQL lentes PrestaShop : activer slow query log et, pour une méthode reproductible, Audit performance PrestaShop : méthode en 6 étapes reproductibles.
Une pratique simple et très rentable : définir des SLO internes pour l’export, comme si c’était un micro-service.
| Indicateur | Objectif réaliste | Pourquoi |
|---|---|---|
| Durée export incrémental | < 2–5 min | Limiter charge serveur, réduire fenêtre d’incohérence |
| Durée export complet | < 30–60 min (selon taille) | Éviter qu’un job déborde sur les heures de trafic |
| Mémoire max | bornée et stable | Prévenir OOM / kill et erreurs intermittentes |
| % produits en erreur (export) | ~0% | Un flux “partiellement faux” est pire qu’un flux réduit mais fiable |
| Taux d’approbation Merchant Center | proche du maximum | KPI direct côté business |
Côté runtime PHP, évitez de recalculer des blocs complets à chaque run. Un pattern simple : cache applicatif (ex. Redis) pour les métadonnées quasi-stables (catégories, règles de taxe par pays, mappings d’attributs), et recalcul strict des champs volatils (prix, stock) par lot. Redis est déjà un standard côté PrestaShop pour décharger MySQL et accélérer certains accès : Redis PrestaShop : configurer le cache sur VPS ou serveur dédié. Attention : cacher le prix final sans intégrer le contexte (pays/devise/groupe) est une façon élégante de générer des incohérences.
Enfin, vous avez besoin d’une boucle d’observabilité. (1) Loggez chaque export avec un export_id, le nombre d’items, la durée, la mémoire max, et les erreurs par type ; (2) surveillez le serveur (CPU, load, saturation PHP-FPM, temps MySQL) via un dashboard ; (3) mettez des alertes sur les échecs cron et sur la dégradation de taux d’approbation dans Merchant Center (Diagnostics). Pour la supervision côté shop, vous avez des bases ici : Surveillance PrestaShop : tableau de bord, seuils et réduction des fausses alertes.
Dernier point : la sécurité “réaliste”. Un feed est souvent accessible par URL (donc potentiellement partageable). Selon votre secteur, le catalogue et les prix peuvent être sensibles. Sans tomber dans la parano :
- servez le flux en HTTPS,
- limitez l’accès (token dans l’URL, Basic Auth si compatible avec votre configuration Merchant Center),
- évitez d’y inclure des informations inutiles (marges, données internes),
- et journalisez proprement sans collecter de données personnelles (RGPD).
Et n’oubliez pas le volet sécurité/opérations : un job qui écrit des fichiers ou appelle une API doit s’exécuter avec le moindre privilège, des secrets isolés, et des journaux d’audit exploitables : Automatisation PrestaShop : sécurité RGPD, moindre privilège et journaux d’audit.




