[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fTVWt9SoWRHmyjXdIiSLG3i8FhFXP6HbbNGxLM_YfxcE":3,"$fclcixq-vdmnoMys--sRaidy-1es6TEjd25QZYieUEoM":25},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":23,"updatedAt":24},"69d740f8aa6b273b0c0f028c","a-b-testing-seo-tester-sans-penaliser-le-referencement",0,"Equipe Seogard","Un site e-commerce de 22 000 pages produit décide de tester un nouveau format de balise title sur ses catégories. Le dev pousse la variante B uniquement côté client via un script A/B classique. Googlebot ne voit jamais la variante. Le test dure 8 semaines, les résultats sont \"non concluants\" — normal, le moteur d'indexation n'a jamais crawlé ce que vous pensiez tester. Pire scénario : la variante est servie à Googlebot mais pas aux utilisateurs, et vous venez d'implémenter du cloaking sans le savoir.\n\nL'A/B testing SEO est un exercice d'équilibriste. La frontière entre test légitime et manipulation trompeuse tient à des détails d'implémentation que Google a documentés de façon étonnamment explicite — mais que la plupart des équipes n'appliquent pas correctement.\n\n## Ce que Google autorise (et interdit) explicitement\n\nLa documentation officielle de Google sur l'A/B testing et le SEO existe depuis des années, mais reste régulièrement ignorée. Le [guide Google sur les tests de sites web](https://developers.google.com/search/docs/crawling-indexing/website-testing) pose quatre règles claires :\n\n1. **Pas de cloaking** : la variante servie à Googlebot doit être une variante que de vrais utilisateurs voient aussi. Servir une page entièrement différente au bot est une violation des guidelines.\n2. **Utiliser `rel=\"canonical\"`** : les URL de test doivent pointer vers l'original, ou les variantes ne doivent pas créer d'URL distinctes.\n3. **Préférer les redirections 302** pour les tests temporaires basés sur des URL différentes (pas des 301 qui signalent un déplacement permanent).\n4. **Durée raisonnable** : ne pas laisser un test tourner indéfiniment. Google ne donne pas de seuil précis, mais mentionne explicitement que la durée doit être \"aussi longue que nécessaire\" et pas plus.\n\n### La zone grise du cloaking\n\nLe cloaking, c'est montrer un contenu différent à Googlebot et aux utilisateurs. Mais un A/B test montre *par définition* des contenus différents à différents utilisateurs. La distinction tient à un point précis : **Googlebot doit être traité comme un participant normal du test**, pas comme un segment spécial.\n\nSi votre outil d'A/B testing assigne les visiteurs à des groupes via un cookie, Googlebot (qui ne conserve pas les cookies entre les crawls) verra alternativement la variante A et la variante B. C'est exactement ce que Google considère comme acceptable.\n\nCe qui n'est *pas* acceptable :\n\n- Détecter le user-agent Googlebot pour forcer une variante spécifique\n- Servir la variante B uniquement côté client (JavaScript) sur un site dont le contenu critique est rendu côté serveur — Googlebot avec le Web Rendering Service (WRS) exécute le JS, mais avec des délais et des particularités qui faussent le test\n- Créer des URL de variante (`/categorie?variant=b`) indexables sans canonical vers l'original\n\nCe dernier point est un piège classique de [contenu dupliqué](/blog/contenu-duplique-causes-techniques-et-solutions) que les plateformes de testing génèrent silencieusement.\n\n## Les trois architectures de test SEO et leurs trade-offs\n\nIl n'existe pas une seule façon de faire de l'A/B testing en SEO. Chaque méthode a ses implications techniques.\n\n### Split testing par URL (server-side redirect)\n\nLe principe : 50 % du trafic sur `/categorie-chaussures` est redirigé (302) vers `/categorie-chaussures-v2`. Chaque URL a son propre contenu.\n\n```nginx\n# Nginx — split test 50/50 via split_clients\nsplit_clients \"${remote_addr}\" $variant {\n    50%    \"original\";\n    *      \"test\";\n}\n\nserver {\n    listen 443 ssl;\n    server_name www.monsite-ecommerce.fr;\n\n    location /categorie-chaussures {\n        if ($variant = \"test\") {\n            return 302 /categorie-chaussures-v2;\n        }\n        # Sert la version originale\n        proxy_pass http://backend;\n    }\n\n    location /categorie-chaussures-v2 {\n        proxy_pass http://backend;\n        # Header X-Robots-Tag pour empêcher l'indexation de la variante\n        add_header X-Robots-Tag \"noindex\" always;\n    }\n}\n```\n\n**Avantages** : test pur côté serveur. Googlebot participe au test comme n'importe quel visiteur. Résultats mesurables directement dans Search Console en comparant les deux URL.\n\n**Inconvénients** : crée une URL supplémentaire indexable si vous oubliez le `noindex` ou le `rel=\"canonical\"`. Consomme du [crawl budget](/blog/mega-menus-et-seo-attention-au-crawl-budget) si appliqué sur des milliers de pages. La directive `split_clients` de Nginx utilise l'IP, donc Googlebot (qui crawle depuis un nombre limité de plages IP) pourrait être surreprésenté dans un groupe.\n\n**Recommandation** : réservez cette méthode aux tests sur un petit groupe de pages (10-50 URL). Ajoutez systématiquement un canonical sur la variante vers l'original.\n\n### Split testing par contenu (même URL, server-side)\n\nLe principe : l'URL reste identique, mais le serveur sert un contenu différent selon un cookie ou un tirage aléatoire. C'est ce que font la plupart des outils d'A/B testing server-side (LaunchDarkly, Kameleoon côté serveur, Optimizely Full Stack).\n\n```typescript\n// Next.js middleware — A/B test sur les titles des pages catégories\nimport { NextRequest, NextResponse } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  const url = request.nextUrl;\n\n  // Ne tester que les pages catégories\n  if (!url.pathname.startsWith('/categorie-')) {\n    return NextResponse.next();\n  }\n\n  // Lire le cookie existant ou assigner un groupe\n  let variant = request.cookies.get('ab_title_test')?.value;\n\n  if (!variant) {\n    variant = Math.random() \u003C 0.5 ? 'control' : 'variant_b';\n    const response = NextResponse.next();\n    response.cookies.set('ab_title_test', variant, {\n      maxAge: 60 * 60 * 24 * 30, // 30 jours\n      httpOnly: true,\n      sameSite: 'lax',\n    });\n    // Passer la variante au composant via un header custom\n    response.headers.set('x-ab-variant', variant);\n    return response;\n  }\n\n  const response = NextResponse.next();\n  response.headers.set('x-ab-variant', variant);\n  return response;\n}\n```\n\nCôté rendu, le composant page lit le header `x-ab-variant` et adapte le `\u003Ctitle>` :\n\n```tsx\n// app/categorie-[slug]/page.tsx\nimport { headers } from 'next/headers';\n\nexport async function generateMetadata({ params }) {\n  const headersList = headers();\n  const variant = headersList.get('x-ab-variant') || 'control';\n  const category = await getCategory(params.slug);\n\n  const titles = {\n    control: `${category.name} — Achat en ligne | MonSite`,\n    variant_b: `${category.name} : ${category.productCount} produits dès ${category.minPrice}€`,\n  };\n\n  return {\n    title: titles[variant],\n    // Le canonical reste TOUJOURS l'URL canonique, pas de paramètre de variante\n    alternates: {\n      canonical: `https://www.monsite-ecommerce.fr/categorie-${params.slug}`,\n    },\n  };\n}\n```\n\n**Point critique** : Googlebot ne conserve pas les cookies entre les crawls. À chaque visite, il sera réassigné aléatoirement. Il verra donc tantôt la variante A, tantôt la variante B. C'est exactement le comportement que Google considère comme légitime — le bot est traité comme un utilisateur normal sans cookie.\n\nC'est aussi là qu'apparaît une subtilité que peu d'articles mentionnent : si Googlebot crawle une page 20 fois pendant votre test, il verra un mélange des deux variantes. L'indexation résultante sera imprévisible — Google choisira le contenu qu'il a vu le plus récemment ou le plus souvent. C'est pourquoi la durée du test compte : plus le test dure, plus vous multipliez les versions crawlées, et plus le signal envoyé à l'index est bruité.\n\n### Test client-side (JavaScript only)\n\nLe principe : le HTML initial est identique pour tous. Un script JavaScript modifie le DOM après chargement (changement de title, réorganisation de blocs, modification de H1).\n\n**C'est la méthode la plus risquée pour le SEO**, et pourtant la plus utilisée (Google Optimize, avant sa disparition, fonctionnait ainsi ; VWO et AB Tasty aussi par défaut).\n\nLe problème fondamental : si votre site est en SSR et que le contenu critique (titles, H1, contenu texte) est dans le HTML initial, un test client-side qui modifie ces éléments crée une [divergence SSR/CSR](/blog/comparer-ssr-et-csr-detecter-les-divergences-invisibles). Googlebot, via le WRS, exécute le JavaScript — mais avec un délai. Google a confirmé à plusieurs reprises que le rendu JS peut prendre de quelques heures à plusieurs jours après le crawl initial.\n\nConcrètement, cela signifie que pendant la fenêtre entre le crawl HTML et le rendu JS, Google indexe le contenu SSR (variante A). Quand le JS est finalement exécuté, si l'utilisateur était assigné à la variante B, le contenu change. Google doit alors réconcilier les deux versions. Le résultat est imprévisible.\n\n**Quand le test client-side est acceptable** : pour des éléments qui n'impactent pas l'indexation. Couleur d'un bouton CTA, position d'un formulaire, modification d'images non critiques. Pour tout ce qui touche au contenu textuel indexé, passez en server-side.\n\n## Scénario concret : test de titles sur 1 200 pages catégories\n\nVoici un cas réaliste. Un site e-commerce mode avec 18 000 pages (1 200 catégories, 16 800 fiches produit) veut tester un nouveau format de title sur ses catégories. Hypothèse : ajouter le nombre de produits et le prix plancher dans le title augmentera le CTR organique.\n\n### Configuration du test\n\n- **Groupe test** : 600 catégories (les catégories paires par ID interne)\n- **Groupe contrôle** : 600 catégories (les catégories impaires)\n- **Durée cible** : 4 semaines\n- **Méthode** : split par contenu server-side (même URL, title différent selon le groupe)\n- **Mesure** : CTR dans Search Console, comparé entre les deux groupes\n\n### Différence avec un A/B test classique\n\nAttention : ce n'est pas un A/B test au sens statistique classique (même page, deux variantes aléatoires pour l'utilisateur). C'est un **split test SEO** où des pages différentes reçoivent des traitements différents. La randomisation se fait au niveau de la page, pas au niveau du visiteur — parce que Google indexe des pages, pas des sessions.\n\nCette nuance est fondamentale. En A/B testing SEO, vous comparez les performances organiques de deux groupes de pages. Chaque page a un seul title (pas d'alternance aléatoire pour le bot). L'assignation est déterministe : la page X est toujours dans le groupe test.\n\n```python\n# Script d'assignation déterministe pour split test SEO\n# Utilise l'ID catégorie pour une assignation stable\n\nimport hashlib\n\ndef assign_variant(category_id: int, test_name: str) -> str:\n    \"\"\"\n    Assignation déterministe basée sur un hash.\n    Avantage vs pair/impair : distribution uniforme même si les IDs\n    ne sont pas séquentiels, et facile à re-bucketer par test.\n    \"\"\"\n    hash_input = f\"{test_name}:{category_id}\"\n    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)\n    return \"variant_b\" if hash_value % 2 == 0 else \"control\"\n\n# Vérification de la distribution\nfrom collections import Counter\ncategory_ids = range(1, 1201)\ndistribution = Counter(\n    assign_variant(cid, \"title_ctr_q2_2026\") for cid in category_ids\n)\nprint(distribution)\n# Counter({'control': 598, 'variant_b': 602}) — distribution quasi-parfaite\n```\n\n### Monitoring pendant le test\n\nQuatre semaines de test sur 600 URL, c'est environ 2 400 à 6 000 crawls de Googlebot sur le groupe test (en estimant 4 à 10 crawls par URL de catégorie sur cette période — vérifié via les logs serveur). Voici ce qu'il faut monitorer :\n\n**1. Vérifier que Google indexe bien le nouveau title**\n\nDans [Search Console](/blog/google-search-console-les-rapports-que-vous-ignorez), utilisez le rapport Performances filtré par page. Comparez les impressions et CTR des deux groupes. Si Google n'affiche pas le nouveau title dans les SERPs après 10 jours, il y a un problème d'indexation.\n\nOutil complémentaire : l'outil d'inspection d'URL de Search Console. Testez 5-10 URL du groupe test pour vérifier que le title indexé correspond bien à la variante B.\n\n**2. Surveiller les régressions**\n\nUn split test mal implémenté peut causer des erreurs silencieuses : canonical manquant sur certaines pages, title vide pour certains edge cases (catégorie sans produit → \"0 produits dès €\"), duplication accidentelle. Ce type de [régression SEO](/blog/regressions-seo-les-10-types-les-plus-frequents) est exactement ce qu'un monitoring automatisé détecte avant que l'impact ne soit visible dans les métriques de trafic. Un outil comme Seogard qui crawle quotidiennement vos pages critiques repérera un title manquant ou une modification inattendue dans les heures qui suivent le déploiement.\n\n**3. Vérifier l'absence de cloaking accidentel**\n\nAvec [Chrome DevTools](/blog/chrome-devtools-pour-le-seo-astuces-avancees), comparez le HTML retourné avec et sans cookie d'assignation. En ligne de commande :\n\n```bash\n# Récupérer le HTML tel que Googlebot le verrait (sans cookie)\ncurl -s -A \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\" \\\n  \"https://www.monsite-ecommerce.fr/categorie-robes\" | \\\n  grep -o '\u003Ctitle>[^\u003C]*\u003C/title>'\n\n# Récupérer le HTML avec cookie de variante B\ncurl -s -b \"ab_title_test=variant_b\" \\\n  \"https://www.monsite-ecommerce.fr/categorie-robes\" | \\\n  grep -o '\u003Ctitle>[^\u003C]*\u003C/title>'\n\n# Récupérer le HTML avec cookie de contrôle\ncurl -s -b \"ab_title_test=control\" \\\n  \"https://www.monsite-ecommerce.fr/categorie-robes\" | \\\n  grep -o '\u003Ctitle>[^\u003C]*\u003C/title>'\n```\n\nSi le test est correctement implémenté et que l'assignation est **déterministe par page** (pas par cookie), les trois commandes doivent retourner le même title — celui assigné à cette page spécifique. Googlebot sans cookie voit exactement la même chose qu'un utilisateur avec cookie, parce que le title est fixé par l'ID de la catégorie, pas par le visiteur.\n\n### Résultats et décision\n\nAprès 4 semaines, le groupe test montre un CTR moyen de 3.8 % contre 3.1 % pour le contrôle. Avant de déployer, vérifiez :\n\n- La significativité statistique (un test sur 600 pages avec ~50K impressions par groupe sur 4 semaines est généralement suffisant)\n- Que le lift CTR n'est pas accompagné d'une baisse de position moyenne (un title plus \"commercial\" peut améliorer le CTR mais signaler à Google un changement de pertinence)\n- Que le trafic global du groupe test n'a pas baissé malgré le meilleur CTR (cas possible si les impressions ont chuté)\n\n## Pièges spécifiques au testing sur le contenu indexé\n\n### Le Vary header oublié\n\nSi vous servez un contenu différent selon un cookie depuis le même URL, ajoutez le header `Vary: Cookie`. Sans cela, un CDN intermédiaire risque de cacher une seule variante et de la servir à tous — y compris Googlebot.\n\n```\nVary: Cookie\n```\n\nEn pratique, dans le cas d'un split test SEO par page (assignation déterministe basée sur l'ID), ce problème ne se pose pas : chaque URL a un seul contenu. Le `Vary` est nécessaire uniquement si vous faites un vrai A/B par utilisateur sur la même URL.\n\n### Les tests qui durent trop longtemps\n\nGoogle ne donne pas de durée maximale. Mais voici le raisonnement : un test qui dure 3 mois sur 1 200 pages, c'est 1 200 URL dont le title \"fluctue\" (dans le cas d'un A/B par utilisateur) ou diffère de l'historique (dans le cas d'un split). Si le test est un split par page, les 600 pages du groupe test ont un title stable et différent de l'original pendant toute la durée — c'est clean.\n\nLe vrai risque de durée concerne les A/B par utilisateur où Googlebot voit alternativement deux versions. Plus c'est long, plus Google accumule des signaux contradictoires pour l'indexation de cette URL.\n\nRègle pragmatique : 4 à 6 semaines maximum pour un test SEO. C'est suffisant pour atteindre la significativité statistique sur un site avec un trafic organique décent (10K+ sessions/semaine sur les pages testées).\n\n### L'interaction avec le cache de rendu JavaScript\n\nSi votre site utilise un framework JavaScript (React, Vue, Angular) et que vous faites du SSR ou de l'ISR (Incremental Static Regeneration), le test doit être intégré au pipeline de rendu serveur — pas ajouté en couche client après le rendu.\n\nAvec Next.js en ISR, la page est générée statiquement puis re-générée à intervalle. Si votre logique de test est dans le middleware (comme dans l'exemple plus haut), le middleware s'exécute à chaque requête, mais la page elle-même peut être servie depuis le cache. Résultat : le header `x-ab-variant` change, mais le contenu de la page reste celui du cache précédent.\n\nSolution : pour un split test SEO (assignation par page), intégrez la logique directement dans `generateMetadata` sans dépendre d'un header de requête. L'ID de la catégorie suffit à déterminer la variante.\n\n## Edge SEO : déployer des tests au niveau CDN\n\nL'approche [Edge SEO](/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn) permet d'injecter des modifications au niveau du CDN (Cloudflare Workers, Fastly VCL, Akamai EdgeWorkers) sans toucher au code applicatif. C'est particulièrement utile pour des équipes SEO qui n'ont pas accès au déploiement backend.\n\nUn Cloudflare Worker peut réécrire le `\u003Ctitle>` en streaming, sans bloquer le TTFB :\n\n```javascript\n// Cloudflare Worker — réécriture de title pour split test SEO\nexport default {\n  async fetch(request, env) {\n    const url = new URL(request.url);\n\n    // Ne cibler que les pages catégories\n    if (!url.pathname.match(/^\\/categorie-/)) {\n      return fetch(request);\n    }\n\n    const response = await fetch(request);\n\n    // Extraire l'ID catégorie depuis un header custom ou le HTML\n    const categorySlug = url.pathname.replace('/categorie-', '');\n    const variant = assignVariant(categorySlug, 'title_test_q2');\n\n    if (variant !== 'variant_b') {\n      return response;\n    }\n\n    // Réécrire le title via HTMLRewriter (API native Cloudflare)\n    return new HTMLRewriter()\n      .on('title', {\n        text(text) {\n          if (text.text.includes('| MonSite')) {\n            // Ajouter le nombre de produits avant la marque\n            // (idéalement récupéré via une API ou un KV store)\n            text.replace(\n              text.text.replace('| MonSite', '— 247 produits dès 19€ | MonSite')\n            );\n          }\n        },\n      })\n      .transform(response);\n  },\n};\n\nfunction assignVariant(slug, testName) {\n  // Hash déterministe simple côté edge\n  let hash = 0;\n  const input = `${testName}:${slug}`;\n  for (let i = 0; i \u003C input.length; i++) {\n    hash = (hash \u003C\u003C 5) - hash + input.charCodeAt(i);\n    hash |= 0;\n  }\n  return hash % 2 === 0 ? 'variant_b' : 'control';\n}\n```\n\n**Avantage majeur** : le test est transparent pour le backend. Rollback instantané en désactivant le Worker. Googlebot reçoit le HTML modifié directement — pas de dépendance au rendu JS.\n\n**Risque** : si le Worker est mal configuré, il peut réécrire des pages non ciblées, créer des titles tronqués, ou interférer avec d'autres modifications edge. Monitorez les pages modifiées avec un crawl quotidien de vos URL critiques via Screaming Frog ou un outil de [monitoring d'alertes SEO](/blog/alertes-seo-quels-seuils-et-quelle-frequence).\n\n## Mesurer les résultats : les métriques qui comptent\n\nLes métriques d'un test A/B SEO ne sont pas les mêmes que celles d'un test UX classique.\n\n### CTR organique (Search Console)\n\nC'est la métrique primaire pour les tests de titles et meta descriptions. Exportez les données via l'[API Search Console](/blog/search-console-api-automatiser-le-reporting-seo) en filtrant par les URL des groupes test et contrôle. Comparez le CTR moyen pondéré par les impressions.\n\nPiège : le CTR dans Search Console est calculé sur les 28 derniers jours glissants. Si votre test dure 4 semaines, la première semaine de données post-déploiement est mélangée avec les données pré-test. Attendez au moins 10 jours avant de commencer à lire les résultats, et comparez la troisième et quatrième semaine entre elles, pas avec la période pré-test.\n\n### Position moyenne\n\nUn changement de title peut affecter la position, pas seulement le CTR. Si la position moyenne du groupe test baisse significativement (>0.5 position), le title modifié a peut-être réduit la pertinence perçue par Google pour certaines requêtes. C'est un signal d'alerte même si le CTR augmente.\n\n### Clics absolus et impressions\n\nLe CTR peut augmenter si les impressions baissent (vous n'apparaissez plus que sur des requêtes très ciblées). Surveillez les clics absolus et les impressions en parallèle. Un bon test montre une augmentation des clics avec des impressions stables.\n\n### KPIs business downstream\n\nLe CTR ne suffit pas. Suivez le [parcours complet](/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre) : taux de rebond, taux de conversion, revenu par session organique. Un title aguicheur peut augmenter le CTR mais attirer des visiteurs mal qualifiés — le taux de rebond explose et les conversions stagnent.\n\n## Checklist pré-déploiement\n\nAvant de lancer un test A/B SEO, validez chaque point :\n\n**Implémentation technique :**\n- L'assignation est-elle déterministe par page (split test) ou par utilisateur (vrai A/B) ? Les deux sont valides, mais les implications SEO diffèrent.\n- Googlebot est-il traité comme un participant normal, sans détection de user-agent ?\n- Les URL canoniques pointent-elles toutes vers l'URL originale ?\n- Aucune URL de variante supplémentaire n'est créée (ou elles sont en `noindex` + `rel=\"canonical\"`) ?\n- Le header `Vary` est configuré si le contenu varie par cookie sur la même URL ?\n\n**Monitoring :**\n- Un crawl de référence des pages testées a été effectué *avant* le déploiement (Screaming Frog export ou crawl Seogard)\n- Les [checks SEO sont intégrés au CI/CD](/blog/automatiser-les-checks-seo-dans-le-ci-cd) pour détecter les régressions de title/meta\n- Un dashboard Search Console filtre les URL test et contrôle séparément\n- Les logs serveur sont analysables pour vérifier la fréquence de crawl de Googlebot sur les pages testées\n\n**Durée et rollback :**\n- Durée maximale définie (4-6 semaines)\n- Procédure de rollback documentée et testée (désactivation du Worker, revert du feature flag, etc.)\n- Critère d'arrêt d'urgence défini (chute de trafic > 15 % sur le groupe test → rollback immédiat)\n\nL'A/B testing SEO reste l'un des rares moyens de prendre des décisions d'optimisation basées sur des données plutôt que sur l'intuition. La difficulté n'est pas dans la statistique — c'est dans l'implémentation technique qui respecte les contraintes du crawl et de l'indexation. Un test bien implémenté avec un monitoring continu des pages modifiées élimine le risque de régression invisible. C'est exactement le type de surveillance que Seogard automatise : détecter en quelques heures qu'un title a changé, disparu, ou diverge entre le rendu serveur et le rendu client — avant que Google n'indexe le problème.\n```","https://seogard.io/blog/a-b-testing-seo-tester-sans-penaliser-le-referencement","Edge SEO","2026-04-09T06:02:32.665Z","2026-04-09","Méthodes concrètes pour mener des tests A/B SEO conformes aux guidelines Google, sans cloaking ni régression de positionnement.","\u003Cp>Un site e-commerce de 22 000 pages produit décide de tester un nouveau format de balise title sur ses catégories. Le dev pousse la variante B uniquement côté client via un script A/B classique. Googlebot ne voit jamais la variante. Le test dure 8 semaines, les résultats sont \"non concluants\" — normal, le moteur d'indexation n'a jamais crawlé ce que vous pensiez tester. Pire scénario : la variante est servie à Googlebot mais pas aux utilisateurs, et vous venez d'implémenter du cloaking sans le savoir.\u003C/p>\n\u003Cp>L'A/B testing SEO est un exercice d'équilibriste. La frontière entre test légitime et manipulation trompeuse tient à des détails d'implémentation que Google a documentés de façon étonnamment explicite — mais que la plupart des équipes n'appliquent pas correctement.\u003C/p>\n\u003Ch2>Ce que Google autorise (et interdit) explicitement\u003C/h2>\n\u003Cp>La documentation officielle de Google sur l'A/B testing et le SEO existe depuis des années, mais reste régulièrement ignorée. Le \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/website-testing\">guide Google sur les tests de sites web\u003C/a> pose quatre règles claires :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Pas de cloaking\u003C/strong> : la variante servie à Googlebot doit être une variante que de vrais utilisateurs voient aussi. Servir une page entièrement différente au bot est une violation des guidelines.\u003C/li>\n\u003Cli>\u003Cstrong>Utiliser \u003Ccode>rel=\"canonical\"\u003C/code>\u003C/strong> : les URL de test doivent pointer vers l'original, ou les variantes ne doivent pas créer d'URL distinctes.\u003C/li>\n\u003Cli>\u003Cstrong>Préférer les redirections 302\u003C/strong> pour les tests temporaires basés sur des URL différentes (pas des 301 qui signalent un déplacement permanent).\u003C/li>\n\u003Cli>\u003Cstrong>Durée raisonnable\u003C/strong> : ne pas laisser un test tourner indéfiniment. Google ne donne pas de seuil précis, mais mentionne explicitement que la durée doit être \"aussi longue que nécessaire\" et pas plus.\u003C/li>\n\u003C/ol>\n\u003Ch3>La zone grise du cloaking\u003C/h3>\n\u003Cp>Le cloaking, c'est montrer un contenu différent à Googlebot et aux utilisateurs. Mais un A/B test montre \u003Cem>par définition\u003C/em> des contenus différents à différents utilisateurs. La distinction tient à un point précis : \u003Cstrong>Googlebot doit être traité comme un participant normal du test\u003C/strong>, pas comme un segment spécial.\u003C/p>\n\u003Cp>Si votre outil d'A/B testing assigne les visiteurs à des groupes via un cookie, Googlebot (qui ne conserve pas les cookies entre les crawls) verra alternativement la variante A et la variante B. C'est exactement ce que Google considère comme acceptable.\u003C/p>\n\u003Cp>Ce qui n'est \u003Cem>pas\u003C/em> acceptable :\u003C/p>\n\u003Cul>\n\u003Cli>Détecter le user-agent Googlebot pour forcer une variante spécifique\u003C/li>\n\u003Cli>Servir la variante B uniquement côté client (JavaScript) sur un site dont le contenu critique est rendu côté serveur — Googlebot avec le Web Rendering Service (WRS) exécute le JS, mais avec des délais et des particularités qui faussent le test\u003C/li>\n\u003Cli>Créer des URL de variante (\u003Ccode>/categorie?variant=b\u003C/code>) indexables sans canonical vers l'original\u003C/li>\n\u003C/ul>\n\u003Cp>Ce dernier point est un piège classique de \u003Ca href=\"/blog/contenu-duplique-causes-techniques-et-solutions\">contenu dupliqué\u003C/a> que les plateformes de testing génèrent silencieusement.\u003C/p>\n\u003Ch2>Les trois architectures de test SEO et leurs trade-offs\u003C/h2>\n\u003Cp>Il n'existe pas une seule façon de faire de l'A/B testing en SEO. Chaque méthode a ses implications techniques.\u003C/p>\n\u003Ch3>Split testing par URL (server-side redirect)\u003C/h3>\n\u003Cp>Le principe : 50 % du trafic sur \u003Ccode>/categorie-chaussures\u003C/code> est redirigé (302) vers \u003Ccode>/categorie-chaussures-v2\u003C/code>. Chaque URL a son propre contenu.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Nginx — split test 50/50 via split_clients\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">split_clients \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">remote_addr\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> $variant {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    50%    \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"original\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    *      \"test\";\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">server\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    listen \u003C/span>\u003Cspan style=\"color:#79B8FF\">443\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ssl;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    server_name \u003C/span>\u003Cspan style=\"color:#E1E4E8\">www.monsite-ecommerce.fr;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    location\u003C/span>\u003Cspan style=\"color:#B392F0\"> /categorie-chaussures \u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ($variant \u003C/span>\u003Cspan style=\"color:#F97583\">= \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"test\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">            return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 302\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> /categorie-chaussures-v2;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        # Sert la version originale\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_pass \u003C/span>\u003Cspan style=\"color:#E1E4E8\">http://backend;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    location\u003C/span>\u003Cspan style=\"color:#B392F0\"> /categorie-chaussures-v2 \u003C/span>\u003Cspan style=\"color:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        proxy_pass \u003C/span>\u003Cspan style=\"color:#E1E4E8\">http://backend;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        # Header X-Robots-Tag pour empêcher l'indexation de la variante\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        add_header \u003C/span>\u003Cspan style=\"color:#E1E4E8\">X-Robots-Tag \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"noindex\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> always;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Avantages\u003C/strong> : test pur côté serveur. Googlebot participe au test comme n'importe quel visiteur. Résultats mesurables directement dans Search Console en comparant les deux URL.\u003C/p>\n\u003Cp>\u003Cstrong>Inconvénients\u003C/strong> : crée une URL supplémentaire indexable si vous oubliez le \u003Ccode>noindex\u003C/code> ou le \u003Ccode>rel=\"canonical\"\u003C/code>. Consomme du \u003Ca href=\"/blog/mega-menus-et-seo-attention-au-crawl-budget\">crawl budget\u003C/a> si appliqué sur des milliers de pages. La directive \u003Ccode>split_clients\u003C/code> de Nginx utilise l'IP, donc Googlebot (qui crawle depuis un nombre limité de plages IP) pourrait être surreprésenté dans un groupe.\u003C/p>\n\u003Cp>\u003Cstrong>Recommandation\u003C/strong> : réservez cette méthode aux tests sur un petit groupe de pages (10-50 URL). Ajoutez systématiquement un canonical sur la variante vers l'original.\u003C/p>\n\u003Ch3>Split testing par contenu (même URL, server-side)\u003C/h3>\n\u003Cp>Le principe : l'URL reste identique, mais le serveur sert un contenu différent selon un cookie ou un tirage aléatoire. C'est ce que font la plupart des outils d'A/B testing server-side (LaunchDarkly, Kameleoon côté serveur, Optimizely Full Stack).\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Next.js middleware — A/B test sur les titles des pages catégories\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { NextRequest, NextResponse } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/server'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> middleware\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> NextRequest\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> url\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.nextUrl;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Ne tester que les pages catégories\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url.pathname.\u003C/span>\u003Cspan style=\"color:#B392F0\">startsWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/categorie-'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Lire le cookie existant ou assigner un groupe\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> variant \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.cookies.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ab_title_test'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)?.value;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">variant) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    variant \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Math.\u003C/span>\u003Cspan style=\"color:#B392F0\">random\u003C/span>\u003Cspan style=\"color:#E1E4E8\">() \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.5\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'control'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'variant_b'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.cookies.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ab_title_test'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, variant, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      maxAge: \u003C/span>\u003Cspan style=\"color:#79B8FF\">60\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 60\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 24\u003C/span>\u003Cspan style=\"color:#F97583\"> *\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 30\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// 30 jours\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      httpOnly: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      sameSite: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'lax'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Passer la variante au composant via un header custom\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'x-ab-variant'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, variant);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'x-ab-variant'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, variant);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Côté rendu, le composant page lit le header \u003Ccode>x-ab-variant\u003C/code> et adapte le \u003Ccode>&#x3C;title>\u003C/code> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/categorie-[slug]/page.tsx\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { headers } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/headers'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> generateMetadata\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> headersList\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> headers\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> variant\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> headersList.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'x-ab-variant'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'control'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> category\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCategory\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(params.slug);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> titles\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    control: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">category\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} — Achat en ligne | MonSite`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    variant_b: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">category\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">name\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} : ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">category\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">productCount\u003C/span>\u003Cspan style=\"color:#9ECBFF\">} produits dès ${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">category\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">minPrice\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}€`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: titles[variant],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Le canonical reste TOUJOURS l'URL canonique, pas de paramètre de variante\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    alternates: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      canonical: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`https://www.monsite-ecommerce.fr/categorie-${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">params\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  };\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Point critique\u003C/strong> : Googlebot ne conserve pas les cookies entre les crawls. À chaque visite, il sera réassigné aléatoirement. Il verra donc tantôt la variante A, tantôt la variante B. C'est exactement le comportement que Google considère comme légitime — le bot est traité comme un utilisateur normal sans cookie.\u003C/p>\n\u003Cp>C'est aussi là qu'apparaît une subtilité que peu d'articles mentionnent : si Googlebot crawle une page 20 fois pendant votre test, il verra un mélange des deux variantes. L'indexation résultante sera imprévisible — Google choisira le contenu qu'il a vu le plus récemment ou le plus souvent. C'est pourquoi la durée du test compte : plus le test dure, plus vous multipliez les versions crawlées, et plus le signal envoyé à l'index est bruité.\u003C/p>\n\u003Ch3>Test client-side (JavaScript only)\u003C/h3>\n\u003Cp>Le principe : le HTML initial est identique pour tous. Un script JavaScript modifie le DOM après chargement (changement de title, réorganisation de blocs, modification de H1).\u003C/p>\n\u003Cp>\u003Cstrong>C'est la méthode la plus risquée pour le SEO\u003C/strong>, et pourtant la plus utilisée (Google Optimize, avant sa disparition, fonctionnait ainsi ; VWO et AB Tasty aussi par défaut).\u003C/p>\n\u003Cp>Le problème fondamental : si votre site est en SSR et que le contenu critique (titles, H1, contenu texte) est dans le HTML initial, un test client-side qui modifie ces éléments crée une \u003Ca href=\"/blog/comparer-ssr-et-csr-detecter-les-divergences-invisibles\">divergence SSR/CSR\u003C/a>. Googlebot, via le WRS, exécute le JavaScript — mais avec un délai. Google a confirmé à plusieurs reprises que le rendu JS peut prendre de quelques heures à plusieurs jours après le crawl initial.\u003C/p>\n\u003Cp>Concrètement, cela signifie que pendant la fenêtre entre le crawl HTML et le rendu JS, Google indexe le contenu SSR (variante A). Quand le JS est finalement exécuté, si l'utilisateur était assigné à la variante B, le contenu change. Google doit alors réconcilier les deux versions. Le résultat est imprévisible.\u003C/p>\n\u003Cp>\u003Cstrong>Quand le test client-side est acceptable\u003C/strong> : pour des éléments qui n'impactent pas l'indexation. Couleur d'un bouton CTA, position d'un formulaire, modification d'images non critiques. Pour tout ce qui touche au contenu textuel indexé, passez en server-side.\u003C/p>\n\u003Ch2>Scénario concret : test de titles sur 1 200 pages catégories\u003C/h2>\n\u003Cp>Voici un cas réaliste. Un site e-commerce mode avec 18 000 pages (1 200 catégories, 16 800 fiches produit) veut tester un nouveau format de title sur ses catégories. Hypothèse : ajouter le nombre de produits et le prix plancher dans le title augmentera le CTR organique.\u003C/p>\n\u003Ch3>Configuration du test\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Groupe test\u003C/strong> : 600 catégories (les catégories paires par ID interne)\u003C/li>\n\u003Cli>\u003Cstrong>Groupe contrôle\u003C/strong> : 600 catégories (les catégories impaires)\u003C/li>\n\u003Cli>\u003Cstrong>Durée cible\u003C/strong> : 4 semaines\u003C/li>\n\u003Cli>\u003Cstrong>Méthode\u003C/strong> : split par contenu server-side (même URL, title différent selon le groupe)\u003C/li>\n\u003Cli>\u003Cstrong>Mesure\u003C/strong> : CTR dans Search Console, comparé entre les deux groupes\u003C/li>\n\u003C/ul>\n\u003Ch3>Différence avec un A/B test classique\u003C/h3>\n\u003Cp>Attention : ce n'est pas un A/B test au sens statistique classique (même page, deux variantes aléatoires pour l'utilisateur). C'est un \u003Cstrong>split test SEO\u003C/strong> où des pages différentes reçoivent des traitements différents. La randomisation se fait au niveau de la page, pas au niveau du visiteur — parce que Google indexe des pages, pas des sessions.\u003C/p>\n\u003Cp>Cette nuance est fondamentale. En A/B testing SEO, vous comparez les performances organiques de deux groupes de pages. Chaque page a un seul title (pas d'alternance aléatoire pour le bot). L'assignation est déterministe : la page X est toujours dans le groupe test.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Script d'assignation déterministe pour split test SEO\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Utilise l'ID catégorie pour une assignation stable\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hashlib\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C/span>\u003Cspan style=\"color:#B392F0\"> assign_variant\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(category_id: \u003C/span>\u003Cspan style=\"color:#79B8FF\">int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, test_name: \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) -> \u003C/span>\u003Cspan style=\"color:#79B8FF\">str\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Assignation déterministe basée sur un hash.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    Avantage vs pair/impair : distribution uniforme même si les IDs\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    ne sont pas séquentiels, et facile à re-bucketer par test.\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    hash_input \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#F97583\"> f\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">test_name\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\">{\u003C/span>\u003Cspan style=\"color:#E1E4E8\">category_id\u003C/span>\u003Cspan style=\"color:#79B8FF\">}\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    hash_value \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> int\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(hashlib.md5(hash_input.encode()).hexdigest(), \u003C/span>\u003Cspan style=\"color:#79B8FF\">16\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"variant_b\"\u003C/span>\u003Cspan style=\"color:#F97583\"> if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hash_value \u003C/span>\u003Cspan style=\"color:#F97583\">%\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 2\u003C/span>\u003Cspan style=\"color:#F97583\"> ==\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> else\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"control\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérification de la distribution\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> collections \u003C/span>\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Counter\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">category_ids \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> range\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#79B8FF\">1201\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">distribution \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> Counter(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    assign_variant(cid, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"title_ctr_q2_2026\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cid \u003C/span>\u003Cspan style=\"color:#F97583\">in\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> category_ids\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(distribution)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Counter({'control': 598, 'variant_b': 602}) — distribution quasi-parfaite\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch3>Monitoring pendant le test\u003C/h3>\n\u003Cp>Quatre semaines de test sur 600 URL, c'est environ 2 400 à 6 000 crawls de Googlebot sur le groupe test (en estimant 4 à 10 crawls par URL de catégorie sur cette période — vérifié via les logs serveur). Voici ce qu'il faut monitorer :\u003C/p>\n\u003Cp>\u003Cstrong>1. Vérifier que Google indexe bien le nouveau title\u003C/strong>\u003C/p>\n\u003Cp>Dans \u003Ca href=\"/blog/google-search-console-les-rapports-que-vous-ignorez\">Search Console\u003C/a>, utilisez le rapport Performances filtré par page. Comparez les impressions et CTR des deux groupes. Si Google n'affiche pas le nouveau title dans les SERPs après 10 jours, il y a un problème d'indexation.\u003C/p>\n\u003Cp>Outil complémentaire : l'outil d'inspection d'URL de Search Console. Testez 5-10 URL du groupe test pour vérifier que le title indexé correspond bien à la variante B.\u003C/p>\n\u003Cp>\u003Cstrong>2. Surveiller les régressions\u003C/strong>\u003C/p>\n\u003Cp>Un split test mal implémenté peut causer des erreurs silencieuses : canonical manquant sur certaines pages, title vide pour certains edge cases (catégorie sans produit → \"0 produits dès €\"), duplication accidentelle. Ce type de \u003Ca href=\"/blog/regressions-seo-les-10-types-les-plus-frequents\">régression SEO\u003C/a> est exactement ce qu'un monitoring automatisé détecte avant que l'impact ne soit visible dans les métriques de trafic. Un outil comme Seogard qui crawle quotidiennement vos pages critiques repérera un title manquant ou une modification inattendue dans les heures qui suivent le déploiement.\u003C/p>\n\u003Cp>\u003Cstrong>3. Vérifier l'absence de cloaking accidentel\u003C/strong>\u003C/p>\n\u003Cp>Avec \u003Ca href=\"/blog/chrome-devtools-pour-le-seo-astuces-avancees\">Chrome DevTools\u003C/a>, comparez le HTML retourné avec et sans cookie d'assignation. En ligne de commande :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Récupérer le HTML tel que Googlebot le verrait (sans cookie)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -A\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"https://www.monsite-ecommerce.fr/categorie-robes\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>[^&#x3C;]*&#x3C;/title>'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Récupérer le HTML avec cookie de variante B\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -b\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"ab_title_test=variant_b\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"https://www.monsite-ecommerce.fr/categorie-robes\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>[^&#x3C;]*&#x3C;/title>'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Récupérer le HTML avec cookie de contrôle\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -b\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"ab_title_test=control\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  \"https://www.monsite-ecommerce.fr/categorie-robes\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;title>[^&#x3C;]*&#x3C;/title>'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Si le test est correctement implémenté et que l'assignation est \u003Cstrong>déterministe par page\u003C/strong> (pas par cookie), les trois commandes doivent retourner le même title — celui assigné à cette page spécifique. Googlebot sans cookie voit exactement la même chose qu'un utilisateur avec cookie, parce que le title est fixé par l'ID de la catégorie, pas par le visiteur.\u003C/p>\n\u003Ch3>Résultats et décision\u003C/h3>\n\u003Cp>Après 4 semaines, le groupe test montre un CTR moyen de 3.8 % contre 3.1 % pour le contrôle. Avant de déployer, vérifiez :\u003C/p>\n\u003Cul>\n\u003Cli>La significativité statistique (un test sur 600 pages avec ~50K impressions par groupe sur 4 semaines est généralement suffisant)\u003C/li>\n\u003Cli>Que le lift CTR n'est pas accompagné d'une baisse de position moyenne (un title plus \"commercial\" peut améliorer le CTR mais signaler à Google un changement de pertinence)\u003C/li>\n\u003Cli>Que le trafic global du groupe test n'a pas baissé malgré le meilleur CTR (cas possible si les impressions ont chuté)\u003C/li>\n\u003C/ul>\n\u003Ch2>Pièges spécifiques au testing sur le contenu indexé\u003C/h2>\n\u003Ch3>Le Vary header oublié\u003C/h3>\n\u003Cp>Si vous servez un contenu différent selon un cookie depuis le même URL, ajoutez le header \u003Ccode>Vary: Cookie\u003C/code>. Sans cela, un CDN intermédiaire risque de cacher une seule variante et de la servir à tous — y compris Googlebot.\u003C/p>\n\u003Cpre>\u003Ccode>Vary: Cookie\n\u003C/code>\u003C/pre>\n\u003Cp>En pratique, dans le cas d'un split test SEO par page (assignation déterministe basée sur l'ID), ce problème ne se pose pas : chaque URL a un seul contenu. Le \u003Ccode>Vary\u003C/code> est nécessaire uniquement si vous faites un vrai A/B par utilisateur sur la même URL.\u003C/p>\n\u003Ch3>Les tests qui durent trop longtemps\u003C/h3>\n\u003Cp>Google ne donne pas de durée maximale. Mais voici le raisonnement : un test qui dure 3 mois sur 1 200 pages, c'est 1 200 URL dont le title \"fluctue\" (dans le cas d'un A/B par utilisateur) ou diffère de l'historique (dans le cas d'un split). Si le test est un split par page, les 600 pages du groupe test ont un title stable et différent de l'original pendant toute la durée — c'est clean.\u003C/p>\n\u003Cp>Le vrai risque de durée concerne les A/B par utilisateur où Googlebot voit alternativement deux versions. Plus c'est long, plus Google accumule des signaux contradictoires pour l'indexation de cette URL.\u003C/p>\n\u003Cp>Règle pragmatique : 4 à 6 semaines maximum pour un test SEO. C'est suffisant pour atteindre la significativité statistique sur un site avec un trafic organique décent (10K+ sessions/semaine sur les pages testées).\u003C/p>\n\u003Ch3>L'interaction avec le cache de rendu JavaScript\u003C/h3>\n\u003Cp>Si votre site utilise un framework JavaScript (React, Vue, Angular) et que vous faites du SSR ou de l'ISR (Incremental Static Regeneration), le test doit être intégré au pipeline de rendu serveur — pas ajouté en couche client après le rendu.\u003C/p>\n\u003Cp>Avec Next.js en ISR, la page est générée statiquement puis re-générée à intervalle. Si votre logique de test est dans le middleware (comme dans l'exemple plus haut), le middleware s'exécute à chaque requête, mais la page elle-même peut être servie depuis le cache. Résultat : le header \u003Ccode>x-ab-variant\u003C/code> change, mais le contenu de la page reste celui du cache précédent.\u003C/p>\n\u003Cp>Solution : pour un split test SEO (assignation par page), intégrez la logique directement dans \u003Ccode>generateMetadata\u003C/code> sans dépendre d'un header de requête. L'ID de la catégorie suffit à déterminer la variante.\u003C/p>\n\u003Ch2>Edge SEO : déployer des tests au niveau CDN\u003C/h2>\n\u003Cp>L'approche \u003Ca href=\"/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn\">Edge SEO\u003C/a> permet d'injecter des modifications au niveau du CDN (Cloudflare Workers, Fastly VCL, Akamai EdgeWorkers) sans toucher au code applicatif. C'est particulièrement utile pour des équipes SEO qui n'ont pas accès au déploiement backend.\u003C/p>\n\u003Cp>Un Cloudflare Worker peut réécrire le \u003Ccode>&#x3C;title>\u003C/code> en streaming, sans bloquer le TTFB :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Cloudflare Worker — réécriture de title pour split test SEO\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">env\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> url\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request.url);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Ne cibler que les pages catégories\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url.pathname.\u003C/span>\u003Cspan style=\"color:#B392F0\">match\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#F97583\">^\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">categorie-\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> fetch\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Extraire l'ID catégorie depuis un header custom ou le HTML\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> categorySlug\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url.pathname.\u003C/span>\u003Cspan style=\"color:#B392F0\">replace\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/categorie-'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> variant\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> assignVariant\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(categorySlug, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'title_test_q2'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (variant \u003C/span>\u003Cspan style=\"color:#F97583\">!==\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'variant_b'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Réécrire le title via HTMLRewriter (API native Cloudflare)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> HTMLRewriter\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'title'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">        text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">text\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">          if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (text.text.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'| MonSite'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">            // Ajouter le nombre de produits avant la marque\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">            // (idéalement récupéré via une API ou un KV store)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            text.\u003C/span>\u003Cspan style=\"color:#B392F0\">replace\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">              text.text.\u003C/span>\u003Cspan style=\"color:#B392F0\">replace\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'| MonSite'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'— 247 produits dès 19€ | MonSite'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      })\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">transform\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(response);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">function\u003C/span>\u003Cspan style=\"color:#B392F0\"> assignVariant\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">testName\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Hash déterministe simple côté edge\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hash \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> input\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">testName\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}:${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">let\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> i \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; i \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> input.\u003C/span>\u003Cspan style=\"color:#79B8FF\">length\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; i\u003C/span>\u003Cspan style=\"color:#F97583\">++\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    hash \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (hash \u003C/span>\u003Cspan style=\"color:#F97583\">&#x3C;&#x3C;\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 5\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">-\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hash \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> input.\u003C/span>\u003Cspan style=\"color:#B392F0\">charCodeAt\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(i);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    hash \u003C/span>\u003Cspan style=\"color:#F97583\">|=\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hash \u003C/span>\u003Cspan style=\"color:#F97583\">%\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 2\u003C/span>\u003Cspan style=\"color:#F97583\"> ===\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'variant_b'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'control'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Avantage majeur\u003C/strong> : le test est transparent pour le backend. Rollback instantané en désactivant le Worker. Googlebot reçoit le HTML modifié directement — pas de dépendance au rendu JS.\u003C/p>\n\u003Cp>\u003Cstrong>Risque\u003C/strong> : si le Worker est mal configuré, il peut réécrire des pages non ciblées, créer des titles tronqués, ou interférer avec d'autres modifications edge. Monitorez les pages modifiées avec un crawl quotidien de vos URL critiques via Screaming Frog ou un outil de \u003Ca href=\"/blog/alertes-seo-quels-seuils-et-quelle-frequence\">monitoring d'alertes SEO\u003C/a>.\u003C/p>\n\u003Ch2>Mesurer les résultats : les métriques qui comptent\u003C/h2>\n\u003Cp>Les métriques d'un test A/B SEO ne sont pas les mêmes que celles d'un test UX classique.\u003C/p>\n\u003Ch3>CTR organique (Search Console)\u003C/h3>\n\u003Cp>C'est la métrique primaire pour les tests de titles et meta descriptions. Exportez les données via l'\u003Ca href=\"/blog/search-console-api-automatiser-le-reporting-seo\">API Search Console\u003C/a> en filtrant par les URL des groupes test et contrôle. Comparez le CTR moyen pondéré par les impressions.\u003C/p>\n\u003Cp>Piège : le CTR dans Search Console est calculé sur les 28 derniers jours glissants. Si votre test dure 4 semaines, la première semaine de données post-déploiement est mélangée avec les données pré-test. Attendez au moins 10 jours avant de commencer à lire les résultats, et comparez la troisième et quatrième semaine entre elles, pas avec la période pré-test.\u003C/p>\n\u003Ch3>Position moyenne\u003C/h3>\n\u003Cp>Un changement de title peut affecter la position, pas seulement le CTR. Si la position moyenne du groupe test baisse significativement (>0.5 position), le title modifié a peut-être réduit la pertinence perçue par Google pour certaines requêtes. C'est un signal d'alerte même si le CTR augmente.\u003C/p>\n\u003Ch3>Clics absolus et impressions\u003C/h3>\n\u003Cp>Le CTR peut augmenter si les impressions baissent (vous n'apparaissez plus que sur des requêtes très ciblées). Surveillez les clics absolus et les impressions en parallèle. Un bon test montre une augmentation des clics avec des impressions stables.\u003C/p>\n\u003Ch3>KPIs business downstream\u003C/h3>\n\u003Cp>Le CTR ne suffit pas. Suivez le \u003Ca href=\"/blog/mesurer-l-impact-seo-technique-quels-kpis-suivre\">parcours complet\u003C/a> : taux de rebond, taux de conversion, revenu par session organique. Un title aguicheur peut augmenter le CTR mais attirer des visiteurs mal qualifiés — le taux de rebond explose et les conversions stagnent.\u003C/p>\n\u003Ch2>Checklist pré-déploiement\u003C/h2>\n\u003Cp>Avant de lancer un test A/B SEO, validez chaque point :\u003C/p>\n\u003Cp>\u003Cstrong>Implémentation technique :\u003C/strong>\u003C/p>\n\u003Cul>\n\u003Cli>L'assignation est-elle déterministe par page (split test) ou par utilisateur (vrai A/B) ? Les deux sont valides, mais les implications SEO diffèrent.\u003C/li>\n\u003Cli>Googlebot est-il traité comme un participant normal, sans détection de user-agent ?\u003C/li>\n\u003Cli>Les URL canoniques pointent-elles toutes vers l'URL originale ?\u003C/li>\n\u003Cli>Aucune URL de variante supplémentaire n'est créée (ou elles sont en \u003Ccode>noindex\u003C/code> + \u003Ccode>rel=\"canonical\"\u003C/code>) ?\u003C/li>\n\u003Cli>Le header \u003Ccode>Vary\u003C/code> est configuré si le contenu varie par cookie sur la même URL ?\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Cstrong>Monitoring :\u003C/strong>\u003C/p>\n\u003Cul>\n\u003Cli>Un crawl de référence des pages testées a été effectué \u003Cem>avant\u003C/em> le déploiement (Screaming Frog export ou crawl Seogard)\u003C/li>\n\u003Cli>Les \u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">checks SEO sont intégrés au CI/CD\u003C/a> pour détecter les régressions de title/meta\u003C/li>\n\u003Cli>Un dashboard Search Console filtre les URL test et contrôle séparément\u003C/li>\n\u003Cli>Les logs serveur sont analysables pour vérifier la fréquence de crawl de Googlebot sur les pages testées\u003C/li>\n\u003C/ul>\n\u003Cp>\u003Cstrong>Durée et rollback :\u003C/strong>\u003C/p>\n\u003Cul>\n\u003Cli>Durée maximale définie (4-6 semaines)\u003C/li>\n\u003Cli>Procédure de rollback documentée et testée (désactivation du Worker, revert du feature flag, etc.)\u003C/li>\n\u003Cli>Critère d'arrêt d'urgence défini (chute de trafic > 15 % sur le groupe test → rollback immédiat)\u003C/li>\n\u003C/ul>\n\u003Cp>L'A/B testing SEO reste l'un des rares moyens de prendre des décisions d'optimisation basées sur des données plutôt que sur l'intuition. La difficulté n'est pas dans la statistique — c'est dans l'implémentation technique qui respecte les contraintes du crawl et de l'indexation. Un test bien implémenté avec un monitoring continu des pages modifiées élimine le risque de régression invisible. C'est exactement le type de surveillance que Seogard automatise : détecter en quelques heures qu'un title a changé, disparu, ou diverge entre le rendu serveur et le rendu client — avant que Google n'indexe le problème.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"ab-testing","seo","cloaking","guidelines","edge-seo","A/B testing SEO : tester sans risquer la pénalité","Thu Apr 09 2026 06:02:32 GMT+0000 (Coordinated Universal Time)",[26],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":16,"tags":32,"title":37,"updatedAt":38},"69d708bbaa6b273b0ce21762","edge-seo-modifier-les-reponses-http-au-niveau-cdn","https://seogard.io/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn","2026-04-09T02:02:35.967Z","Cloudflare Workers et Lambda@Edge pour injecter des balises, gérer des redirections et corriger le SEO technique sans toucher au code source.",[22,33,34,35,36],"cloudflare-workers","lambda-edge","cdn","seo-technique","Edge SEO : modifier les réponses HTTP au niveau CDN","Thu Apr 09 2026 02:02:35 GMT+0000 (Coordinated Universal Time)"]