Out-of-stock et Google : nouvelles règles Merchant Center

Un e-commerce de 22 000 références avec 35% de produits temporairement indisponibles. Du jour au lendemain, 7 700 fiches produits risquent une suspension Merchant Center parce que le bouton "Ajouter au panier" n'est pas visuellement désactivé. C'est le scénario concret que les nouvelles règles Google imposent à partir de mars 2026.

Google vient de durcir significativement ses exigences pour les pages produits en rupture de stock. Le changement dépasse le simple ajustement de politique Merchant Center : il touche le HTML, le structured data, le rendu côté serveur et la logique de monitoring. Décortiquons ce que ça implique techniquement.

Ce que Google exige concrètement

La mise à jour de la politique Merchant Center est précise : pour tout produit indisponible, la page doit afficher un bouton d'achat visuellement désactivé (grayed out, disabled), ou supprimer complètement le CTA d'achat. Google ne se contente plus de lire le availability dans le structured data — ses systèmes vérifient maintenant la cohérence entre les données structurées, le rendu visuel de la page et l'état réel du produit.

Pourquoi ce durcissement

Google Shopping et les free product listings génèrent du trafic vers des pages où l'utilisateur ne peut pas acheter. Le taux de rebond explose, l'expérience utilisateur se dégrade, et Google perd en crédibilité comme canal d'acquisition. La documentation Merchant Center sur les exigences de page de destination a été mise à jour pour inclure explicitement cette règle.

Le signal de confiance est central : si votre structured data déclare https://schema.org/OutOfStock mais que le bouton "Acheter" reste cliquable et redirige vers un panier vide ou une erreur, vous êtes en violation. La suspension peut toucher l'ensemble de votre flux Merchant Center, pas seulement les fiches incriminées.

Les trois niveaux de conformité

  1. Minimum requis : le bouton d'achat est disabled au niveau HTML avec un état visuel distinct (opacité réduite, couleur grisée).
  2. Recommandé : le bouton est remplacé par un message explicite ("Produit indisponible — Recevoir une alerte") et le formulaire d'ajout au panier est supprimé du DOM.
  3. Optimal : en plus du point 2, la page affiche une date estimée de retour en stock, propose des alternatives, et le availability dans le structured data reflète précisément l'état (BackOrder, PreOrder, OutOfStock, Discontinued).

Implémentation HTML et accessibilité du bouton désactivé

Le piège classique : désactiver visuellement un bouton en CSS sans toucher le HTML. Google effectue du rendering — il voit le DOM post-JavaScript. Un bouton qui a l'air grisé mais reste fonctionnel dans le DOM sera détecté comme non conforme.

Le pattern correct

<!-- Produit en stock -->
<button type="submit" class="btn-add-to-cart" aria-label="Ajouter Sneakers Air Max 90 au panier">
  Ajouter au panier — 129,90 €
</button>

<!-- Produit hors stock — approche conforme -->
<button type="button" class="btn-add-to-cart btn-add-to-cart--disabled" 
        disabled 
        aria-disabled="true"
        aria-label="Sneakers Air Max 90 actuellement indisponible">
  Indisponible
</button>

<div class="stock-alert" role="complementary">
  <p>Ce produit est temporairement en rupture de stock.</p>
  <form action="/api/stock-alert" method="POST">
    <input type="hidden" name="sku" value="AM90-BLK-42">
    <label for="email-alert">Recevez une alerte de retour en stock :</label>
    <input type="email" id="email-alert" name="email" required placeholder="[email protected]">
    <button type="submit">M'alerter</button>
  </form>
</div>

Trois points critiques dans ce markup :

  • L'attribut disabled sur le <button> empêche l'interaction au niveau du navigateur ET du DOM. Google le détecte.
  • aria-disabled="true" double la sémantique pour les technologies d'assistance et les crawlers qui parsent les attributs ARIA.
  • Le type="button" au lieu de type="submit" empêche toute soumission de formulaire résiduelle si un script retire le disabled par erreur.

L'anti-pattern à éviter

<!-- ❌ Non conforme — désactivation purement visuelle -->
<button type="submit" class="btn-add-to-cart" 
        style="opacity: 0.5; pointer-events: none;">
  Ajouter au panier — 129,90 €
</button>

Ce bouton reste un submit fonctionnel dans le DOM. pointer-events: none est une propriété CSS, pas un attribut HTML sémantique. Le texte dit toujours "Ajouter au panier" avec un prix, ce qui constitue un signal d'achat actif. Les crawlers Merchant Center vont le détecter comme une incohérence avec le structured data OutOfStock.

Structured data Product : la cohérence availability comme impératif

Le schema.org Product supporte plusieurs valeurs pour availability via l'objet Offer. La plupart des implémentations e-commerce n'utilisent que InStock et OutOfStock. Les nouvelles règles Google rendent les valeurs intermédiaires stratégiquement importantes.

Mapping complet des états de stock

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Sneakers Air Max 90 — Noir — Taille 42",
  "sku": "AM90-BLK-42",
  "image": "https://cdn.sneaker-store.fr/am90-blk-42.webp",
  "offers": {
    "@type": "Offer",
    "url": "https://sneaker-store.fr/produits/air-max-90-noir-42",
    "priceCurrency": "EUR",
    "price": "129.90",
    "availability": "https://schema.org/OutOfStock",
    "itemCondition": "https://schema.org/NewCondition",
    "seller": {
      "@type": "Organization",
      "name": "Sneaker Store FR"
    }
  }
}

Les valeurs availability que vous devriez exploiter selon le cas réel :

État réel du produit Valeur schema.org Bouton CTA
Disponible https://schema.org/InStock Actif
Stock limité (<5 unités) https://schema.org/LimitedAvailability Actif + mention "X restants"
Rupture temporaire, réappro prévue https://schema.org/BackOrder Désactivé + date estimée
Pré-commande https://schema.org/PreOrder Actif avec mention pré-commande
Rupture définitive https://schema.org/Discontinued Supprimé + redirection ou alternatives
En magasin uniquement https://schema.org/InStoreOnly Désactivé en ligne + lien store locator

La subtilité que beaucoup ratent : BackOrder et PreOrder permettent de maintenir la visibilité dans Google Shopping tout en étant conforme. Un produit OutOfStock sera délisté des product grids, mais un BackOrder avec une date de disponibilité (availabilityStarts) peut rester visible. C'est un levier de visibilité que la nouvelle segmentation entre organic rankings et product grids rend particulièrement critique.

Pour une implémentation complète du schema Product avec toutes ses propriétés, consultez notre guide sur le product schema pour l'e-commerce.

La cohérence comme facteur de suspension

Google croise trois sources pour valider la cohérence :

  1. Le flux Merchant Center (XML/API) — la valeur availability envoyée dans votre feed.
  2. Le structured data de la page — le JSON-LD parsé lors du crawl.
  3. Le rendu visuel — ce que le renderer Chromium de Google "voit" sur la page.

Si ces trois sources divergent, vous êtes dans la zone de danger. Le cas le plus fréquent : le flux Merchant Center est mis à jour en temps réel via l'API Content, mais le structured data de la page est généré au build time (SSG) et n'est rafraîchi que toutes les 6 heures. Pendant ce delta, l'incohérence est détectable.

Gestion dynamique côté serveur : SSR, ISR et le piège du cache

Pour les sites e-commerce construits sur des frameworks JavaScript (Next.js, Nuxt, SvelteKit), la gestion de l'état de stock dans le markup initial est un défi architectural. Les pièges du SSR pour le SEO e-commerce sont bien documentés, mais le cas spécifique de l'availability ajoute une contrainte de fraîcheur.

Le problème du stale cache

Scénario réaliste : un e-commerce mode avec 18 000 fiches produits sur Next.js, ISR (Incremental Static Regeneration) avec un revalidate de 3600 secondes (1 heure). Un produit tombe en rupture de stock à 10h02. La page statique servie par le CDN affiche encore "Ajouter au panier" (actif) et le structured data déclare InStock jusqu'à 11h02 au plus tôt. Si Googlebot crawle la page entre 10h02 et 11h02, il voit une incohérence avec le flux Merchant Center mis à jour en temps réel.

La solution : revalidation on-demand

// pages/api/stock-webhook.ts (Next.js API Route)
import type { NextApiRequest, NextApiResponse } from 'next';

interface StockUpdatePayload {
  sku: string;
  slug: string;
  availability: 'InStock' | 'OutOfStock' | 'BackOrder' | 'Discontinued';
  updatedAt: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const secret = req.headers['x-revalidation-secret'];
  if (secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ error: 'Invalid secret' });
  }

  const { sku, slug, availability }: StockUpdatePayload = req.body;

  try {
    // Revalider la page produit spécifique
    await res.revalidate(`/produits/${slug}`);
    
    // Revalider la page catégorie parente si le produit passe OutOfStock
    // pour mettre à jour le compteur "X produits disponibles"
    if (availability === 'OutOfStock' || availability === 'Discontinued') {
      const categorySlug = await getCategorySlug(sku);
      if (categorySlug) {
        await res.revalidate(`/categorie/${categorySlug}`);
      }
    }

    // Log pour monitoring
    console.log(
      `[stock-revalidation] SKU=${sku} availability=${availability} at=${new Date().toISOString()}`
    );

    return res.json({ 
      revalidated: true, 
      sku, 
      availability,
      timestamp: new Date().toISOString() 
    });
  } catch (err) {
    console.error(`[stock-revalidation] Failed for SKU=${sku}:`, err);
    return res.status(500).json({ error: 'Revalidation failed' });
  }
}

async function getCategorySlug(sku: string): Promise<string | null> {
  // Votre logique pour retrouver la catégorie parente d'un SKU
  // Ex: requête BDD, appel API PIM, etc.
  return null;
}

Ce webhook se branche sur votre système de gestion de stock (ERP, OMS, PIM). Quand un produit passe en rupture, le système appelle l'endpoint, qui purge le cache ISR de la page produit concernée. La prochaine requête (de Googlebot ou d'un utilisateur) déclenche une régénération avec le bon état de stock.

Pour les sites sur Nuxt.js, le même pattern s'applique avec useAsyncData et l'API de purge de Nitro. Le guide complet est dans notre article sur Nuxt comme solution SSR pour Vue.js et le SEO.

Edge case : les SPA sans SSR

Si vous êtes encore sur une Single Page Application pure (React SPA, Angular sans Universal), le problème est plus grave. Googlebot utilise un renderer Chromium en WRS (Web Rendering Service), mais il y a un délai entre le crawl initial (HTML vide) et le rendering JavaScript. Si votre état de stock est fetchée client-side via une API, le rendu initial ne contient ni le structured data correct ni le bouton dans le bon état. Consultez notre guide complet sur les SPA et le SEO et l'article dédié aux capacités réelles de Google en matière de crawl JavaScript.

Scénario concret : migration d'un catalogue de 22 000 produits

Prenons un cas réaliste. Sneaker Store FR : e-commerce spécialisé, 22 000 fiches produits, plateforme Shopify Plus avec un thème custom. En moyenne, 35% du catalogue est en rupture à un instant T (la mode/sneakers a un turnover de stock élevé). Ça représente ~7 700 pages à traiter.

Audit initial avec Screaming Frog

Première étape : identifier toutes les pages produits avec un état de stock incohérent.

Configuration du crawl dans Screaming Frog :

  1. Custom Extraction pour extraire la valeur availability du JSON-LD :

    • Regex : "availability"\s*:\s*"https?://schema\.org/(\w+)"
    • Scope : dans le <script type="application/ld+json">
  2. Custom Extraction pour détecter l'état du bouton :

    • XPath : //button[contains(@class, 'add-to-cart')]/@disabled
    • Cela retourne disabled si présent, vide sinon.
  3. Custom Search pour trouver les boutons sans attribut disabled sur les pages OutOfStock :

    Exportez les deux extractions, croisez dans un tableur. Toute ligne où availability = OutOfStock ET disabled = vide est non conforme.

Sur le catalogue de Sneaker Store FR, l'audit révèle :

  • 7 700 pages avec availability: OutOfStock dans le JSON-LD
  • 7 200 de ces pages ont encore un bouton "Ajouter au panier" actif (pas de disabled)
  • 340 pages ont un availability: InStock dans le JSON-LD mais le produit est réellement en rupture dans Shopify (le structured data n'a pas été revalidé)
  • 160 pages n'ont aucun structured data Product (thème mal configuré sur certaines variantes)

Plan de remédiation

Phase 1 — Correction du thème Liquid (Shopify) : modifier le template product.liquid pour conditionner l'état du bouton à la variable product.available :

{% if product.available %}
  <button type="submit" name="add" class="btn-add-to-cart"
          data-add-to-cart>
    Ajouter au panier — {{ product.price | money }}
  </button>
{% else %}
  <button type="button" class="btn-add-to-cart btn-add-to-cart--disabled"
          disabled
          aria-disabled="true">
    Indisponible
  </button>
  
  {% comment %} Back-in-stock alert via Klaviyo ou Back In Stock app {% endcomment %}
  <div class="stock-alert-form">
    <p>Retour en stock prévu sous 2-3 semaines.</p>
    {% render 'back-in-stock-form', product: product %}
  </div>
{% endif %}

Phase 2 — Correction du structured data : s'assurer que le JSON-LD est également conditionné :

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": {{ product.title | json }},
  "sku": {{ product.selected_or_first_available_variant.sku | json }},
  "offers": {
    "@type": "Offer",
    "price": {{ product.price | money_without_currency | json }},
    "priceCurrency": "EUR",
    "availability": "https://schema.org/{% if product.available %}InStock{% else %}OutOfStock{% endif %}",
    "url": "{{ shop.url }}{{ product.url }}"
  }
}
</script>

Phase 3 — Synchronisation du flux Merchant Center : sur Shopify Plus, le flux Google Shopping est généralement géré par une app (Google Channel, DataFeedWatch, Channable). Le point critique est de vérifier que la fréquence de mise à jour du flux est inférieure au délai de revalidation des pages. Si le flux est mis à jour toutes les 30 minutes mais que les pages mettent 2 heures à se revalider, vous avez une fenêtre d'incohérence.

Phase 4 — Monitoring continu : c'est ici que l'audit one-shot atteint ses limites. Avec 22 000 produits et un stock qui fluctue quotidiennement, vous avez besoin d'un monitoring qui vérifie en continu la cohérence entre l'état du bouton, le structured data et le flux Merchant Center. Un outil comme Seogard peut détecter automatiquement une régression — par exemple, un déploiement de thème qui réintroduit un bouton actif sur les pages OutOfStock.

Impact mesuré

Après correction sur Sneaker Store FR :

  • Merchant Center : 0 suspension sur les 3 mois suivants (vs. 2 suspensions partielles dans les 6 mois précédents).
  • Crawl budget : pas d'impact direct mesurable. Les pages OutOfStock sont toujours crawlées, mais le taux de crawl des pages InStock augmente mécaniquement quand Google ne perd plus de temps à recrawler des pages incohérentes pour vérifier leur état.
  • Trafic Shopping : les pages en BackOrder avec une date availabilityStarts conservent leur visibilité dans les product grids, là où elles disparaissaient auparavant en OutOfStock.

Le sujet du crawl budget et de son allocation par Googlebot est détaillé dans notre article sur les cinq portes d'infrastructure derrière le crawl, le render et l'index.

Monitoring et détection automatique des régressions

Le vrai risque n'est pas la mise en conformité initiale. C'est la régression silencieuse. Un développeur modifie un composant React, un déploiement de thème écrase une condition Liquid, une mise à jour d'app Shopify réintroduit un bouton actif sur les pages indisponibles.

Ce qu'il faut monitorer

Test 1 — Cohérence structured data / DOM : pour chaque page produit avec availability: OutOfStock dans le JSON-LD, vérifier que le DOM ne contient pas de <button> ou <input type="submit"> actif dans le formulaire d'ajout au panier.

Test 2 — Cohérence flux Merchant / structured data : comparer la valeur availability du flux XML/API avec celle du JSON-LD de la page. Toute divergence de plus de X minutes (seuil dépendant de votre fréquence de mise à jour) doit déclencher une alerte.

Test 3 — Rendu visuel post-JS : certains CMS chargent l'état du bouton via JavaScript après le rendu initial. Le HTML initial peut avoir disabled, mais un script le retire et réactive le bouton. Il faut tester le DOM post-rendering, pas seulement le HTML source.

Vérification via Search Console

L'onglet Shopping dans Google Search Console (pour les sites connectés à Merchant Center) signale désormais les problèmes de "landing page quality". Vérifiez régulièrement les rapports d'état des produits pour repérer les alertes liées à l'incohérence de disponibilité.

La documentation Google sur la résolution des problèmes de page de destination détaille les erreurs spécifiques.

Vérification via Chrome DevTools

Pour un contrôle rapide sur une page spécifique :

// Dans la console Chrome DevTools sur une page produit OutOfStock
// Vérifier l'état du bouton d'ajout au panier

const addToCartButtons = document.querySelectorAll(
  'button[type="submit"][name="add"], .btn-add-to-cart, [data-add-to-cart]'
);

addToCartButtons.forEach((btn, i) => {
  console.log(`Button ${i}:`, {
    disabled: btn.disabled,
    ariaDisabled: btn.getAttribute('aria-disabled'),
    textContent: btn.textContent.trim(),
    type: btn.type,
    isInForm: btn.closest('form') !== null,
    formAction: btn.closest('form')?.action || 'N/A'
  });
});

// Vérifier le structured data
const ldJsonScripts = document.querySelectorAll('script[type="application/ld+json"]');
ldJsonScripts.forEach((script, i) => {
  try {
    const data = JSON.parse(script.textContent);
    if (data['@type'] === 'Product' || data['@type']?.includes('Product')) {
      const offers = Array.isArray(data.offers) ? data.offers : [data.offers];
      offers.forEach(offer => {
        console.log(`Offer availability: ${offer.availability}`);
      });
    }
  } catch (e) {
    console.error(`Invalid JSON-LD in script ${i}:`, e);
  }
});

Ce script identifie instantanément les incohérences sur une page donnée. Pour un audit à l'échelle du catalogue, combinez Screaming Frog (crawl + extraction custom) avec un script Python qui compare le flux Merchant Center aux données extraites.

Cas particuliers et edge cases

Variantes de produit partiellement en stock

Un produit a 5 tailles. Les tailles 40 et 42 sont en stock, les tailles 41, 43 et 44 sont en rupture. Quel availability déclarer ?

La recommandation Google est claire dans la documentation schema.org pour les produits avec variantes : chaque variante doit avoir son propre objet Offer avec sa propre valeur availability. Si vous ne déclarez qu'un seul Offer au niveau du produit parent, utilisez InStock tant qu'au moins une variante est disponible — mais le bouton doit refléter dynamiquement l'état de la variante sélectionnée.

Le piège : l'utilisateur arrive via Google Shopping sur la page produit avec la variante "Taille 43" dans l'URL (?variant=43). Le structured data global dit InStock, mais la variante 43 est en rupture. Le bouton doit être désactivé pour cette variante spécifique. Si votre JavaScript gère le changement d'état du bouton au clic sur le sélecteur de taille mais que le rendu initial (celui que Googlebot voit) affiche le bouton actif avec la variante 43 sélectionnée, vous êtes non conforme.

Pages produits avec lazy loading du stock

Certaines architectures (headless commerce avec Commercetools, BigCommerce headless, etc.) chargent l'état du stock via une API REST ou GraphQL côté client. Le HTML initial ne contient ni l'état du stock ni l'état du bouton. Ces architectures sont intrinsèquement risquées pour cette nouvelle exigence.

La solution : injecter au minimum le structured data côté serveur (SSR ou dans le HTML initial via le backend), et rendre le bouton dans son état disabled par défaut si le produit est connu comme indisponible au moment du rendu serveur. Le JavaScript côté client peut ensuite "activer" le bouton si l'API de stock retourne une disponibilité.

C'est l'inverse du pattern habituel (bouton actif par défaut, désactivé si rupture). Le nouveau paradigme est : bouton inactif par défaut, activé uniquement si le stock est confirmé en temps réel.

Produits définitivement retirés (Discontinued)

Pour les produits retirés définitivement du catalogue, la question se pose : conserver la page (avec une 200 et un structured data Discontinued) ou rediriger (301 vers une alternative ou la catégorie parente) ?

La réponse dépend du trafic organique de la page. Si elle capte du trafic significatif via des requêtes longue traîne, conservez-la en 200 avec des recommandations d'alternatives et un structured data Discontinued. Si elle ne génère pas de trafic, une 301 vers le produit successeur ou la catégorie est plus propre. Dans tous les cas, une gestion correcte des codes de statut HTTP est indispensable, et évitez à tout prix les soft 404 qui gaspillent le crawl budget.

Au-delà de Merchant Center : impact sur le SEO organique

Ce durcissement concerne directement Merchant Center et Google Shopping. Mais les signaux de qualité de page de destination influencent indirectement le SEO organique de deux manières.

Premièrement, les pages produits qui perdent leur éligibilité Shopping perdent aussi les rich results "Product" dans les SERP organiques. Le rich snippet avec prix et disponibilité disparaît, ce qui réduit le CTR organique de la page.

Deuxièmement, une suspension Merchant Center peut déclencher un examen plus large de la qualité du site par les systèmes Google. Ce n'est pas documenté officiellement, mais les signaux de qualité sont de plus en plus transversaux entre les différents services Google (les crawlers non documentés de Google en sont un symptôme).


Le durcissement des règles Google sur les pages out-of-stock n'est pas un ajustement cosmétique. C'est une contrainte d'architecture technique qui impose une cohérence en temps réel entre trois sources de vérité (flux Merchant, structured data, rendu DOM). Les sites qui traitent ça comme un simple fix de template s'exposent à des régressions silencieuses à chaque déploiement. Un monitoring continu — via un outil comme Seogard qui détecte les incohérences de structured data et les changements de DOM — est le seul moyen de rester conforme durablement sur un catalogue de milliers de produits.

Articles connexes

Actualités SEO28 mars 2026

Core Update Mars 2026 : analyse technique et plan d'action

Google déploie la March 2026 Core Update. Analyse technique, scénarios d'impact concrets et méthodologie de diagnostic pour les équipes SEO.

Actualités SEO27 mars 2026

Page Speed : transformer un site lent en machine de course

Guide technique avancé pour optimiser la vitesse de chargement : poids, puissance serveur, navigation du critical path. Code, configs et scénarios réels.

Actualités SEO26 mars 2026

Écrire pour l'IA search : playbook technique du contenu machine-readable

Structurez votre contenu pour que les LLMs l'extraient et le citent. Code, schémas, configs et scénarios concrets pour l'AI search.