[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fByTqfxs362PWSBl0V1dAwQFF5mZ9eOPrCS0mPmmCz7M":3,"$fGRSUph74yZ19VZ7uP70i_k7L9Q65sj66Ye-8klgz_y8":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},"69d708bbaa6b273b0ce21762","edge-seo-modifier-les-reponses-http-au-niveau-cdn",0,"Equipe Seogard","Un site e-commerce de 28 000 pages migre de plateforme. L'équipe marketing découvre 3 semaines après la mise en production que 4 200 pages ont perdu leur balise canonical, que les hreflang ont disparu sur l'ensemble du répertoire `/fr/`, et que 1 600 anciennes URLs renvoient un 200 au lieu d'un 301. Le backlog de la dev team est plein pour 6 sprints. Le trafic organique chute de 18% en 10 jours. L'Edge SEO aurait permis de corriger tout ça en quelques heures, sans toucher à une seule ligne de code applicatif.\n\n## Ce que l'Edge SEO change fondamentalement\n\nL'Edge SEO consiste à intercepter et modifier les réponses HTTP au niveau du CDN — entre le serveur d'origine et le navigateur (ou le crawler). Concrètement, le code s'exécute sur les points de présence (PoPs) du CDN, pas sur votre infrastructure applicative.\n\nCette approche résout un problème organisationnel autant que technique : dans la majorité des organisations, les équipes SEO n'ont pas accès au code source en production. Chaque modification passe par un ticket, une priorisation sprint, une review, un déploiement. Ce cycle peut prendre des semaines pour une balise canonical.\n\n### Les deux plateformes dominantes\n\n**Cloudflare Workers** : code JavaScript/TypeScript exécuté sur le réseau Cloudflare (300+ PoPs). Basé sur le runtime V8 isolates, pas sur Node.js. Le temps de cold start est inférieur à 5ms dans la plupart des cas. La documentation officielle détaille les limites d'exécution : 10ms de CPU time sur le plan gratuit, 50ms sur le plan payant ([Cloudflare Workers Limits](https://developers.cloudflare.com/workers/platform/limits/)).\n\n**Lambda@Edge** : fonctions Lambda déployées sur les edge locations CloudFront d'AWS. Quatre points d'interception : viewer request, origin request, origin response, viewer response. Le cold start est plus élevé (parfois 50-200ms), mais la puissance de calcul disponible est supérieure. La [documentation AWS](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html) spécifie un timeout de 5 secondes pour les events viewer et 30 secondes pour les events origin.\n\nLe choix entre les deux dépend de votre stack existante. Si votre CDN est déjà CloudFront, Lambda@Edge évite une migration. Si vous êtes sur Cloudflare ou si vous partez de zéro, Workers offre une DX (Developer Experience) nettement supérieure : déploiement en secondes via `wrangler`, logs en temps réel, et un modèle de pricing plus prévisible.\n\n### Ce qui est modifiable à l'edge\n\nLa réponse HTTP est un flux que vous pouvez intercepter entièrement. Vous pouvez :\n\n- Ajouter, modifier ou supprimer des headers HTTP (X-Robots-Tag, Link, Hreflang via headers)\n- Réécrire le body HTML (injection de balises `\u003Clink>`, `\u003Cmeta>`, scripts structured data)\n- Implémenter des redirections (301, 302, 308) sans toucher au serveur d'origine\n- Servir un body complètement différent selon le user-agent (pre-rendering sélectif)\n- Modifier le status code de la réponse\n\nLe body rewriting via l'API `HTMLRewriter` de Cloudflare est la fonctionnalité la plus puissante pour le SEO. Elle permet de parser et modifier le HTML en streaming, sans charger tout le document en mémoire.\n\n## Injection de balises SEO avec Cloudflare Workers\n\nLe cas d'usage le plus fréquent : injecter des canonicals, des hreflang, ou du JSON-LD sur des milliers de pages, piloté par un fichier de configuration externe.\n\n### Injecter des canonical dynamiques\n\nPrenons un scénario réaliste : votre plateforme e-commerce génère des URLs avec des paramètres de tri et de filtrage (`?sort=price&color=red`). Le CMS ne gère pas correctement les canonicals sur ces variantes. Plutôt que d'attendre un fix côté back-end, vous déployez un Worker.\n\n```javascript\n// worker-canonical.js\n// Cloudflare Worker : injection de canonical auto-référençant (strip des query params)\n\nconst PARAMS_TO_STRIP = ['sort', 'order', 'color', 'size', 'page', 'ref', 'utm_source', 'utm_medium', 'utm_campaign'];\n\nclass CanonicalHandler {\n  constructor(canonicalUrl) {\n    this.canonicalUrl = canonicalUrl;\n  }\n\n  element(element) {\n    // Supprime toute canonical existante pour éviter les doublons\n    if (element.tagName === 'link' && element.getAttribute('rel') === 'canonical') {\n      element.remove();\n    }\n  }\n}\n\nclass HeadHandler {\n  constructor(canonicalUrl) {\n    this.canonicalUrl = canonicalUrl;\n  }\n\n  element(element) {\n    element.append(`\u003Clink rel=\"canonical\" href=\"${this.canonicalUrl}\" />`, { html: true });\n  }\n}\n\nfunction getCanonicalUrl(url) {\n  const parsed = new URL(url);\n  const params = new URLSearchParams(parsed.search);\n\n  PARAMS_TO_STRIP.forEach(param => params.delete(param));\n\n  const cleanSearch = params.toString();\n  return `${parsed.origin}${parsed.pathname}${cleanSearch ? '?' + cleanSearch : ''}`;\n}\n\nexport default {\n  async fetch(request) {\n    const response = await fetch(request);\n    const contentType = response.headers.get('content-type') || '';\n\n    // Ne traiter que les réponses HTML\n    if (!contentType.includes('text/html')) {\n      return response;\n    }\n\n    const canonicalUrl = getCanonicalUrl(request.url);\n\n    return new HTMLRewriter()\n      .on('link[rel=\"canonical\"]', new CanonicalHandler(canonicalUrl))\n      .on('head', new HeadHandler(canonicalUrl))\n      .transform(response);\n  }\n};\n```\n\nCe Worker fait trois choses : il supprime toute balise canonical existante dans le `\u003Chead>`, il calcule l'URL canonique en strippant les paramètres indésirables, et il injecte la bonne canonical. Le tout en streaming — le TTFB ajouté est négligeable.\n\nLe déploiement est immédiat :\n\n```bash\n# Déploiement via Wrangler CLI\nnpx wrangler deploy worker-canonical.js --name seo-canonical-fix --route \"shop.votredomaine.com/catalog/*\"\n```\n\nLa directive `--route` restreint l'exécution aux URLs du catalogue. C'est critique : un Worker qui s'exécute sur toutes les requêtes (y compris les assets statiques) consomme du CPU inutilement et peut impacter les coûts.\n\n### Injection de hreflang à l'échelle\n\nLes hreflang sont un cauchemar récurrent sur les sites multilingues. Le mapping entre les versions linguistiques change, des pages sont dépubliées dans certaines langues, et les balises deviennent incohérentes. Un problème que l'on retrouve fréquemment dans les [régressions SEO classiques](/blog/regressions-seo-les-10-types-les-plus-frequents).\n\nLa solution edge : un Worker qui lit un mapping depuis un KV store (key-value store intégré à Cloudflare) et injecte les hreflang correspondants.\n\n```javascript\n// worker-hreflang.js\n// Lecture du mapping hreflang depuis Cloudflare KV\n\nclass HreflangInjector {\n  constructor(hreflangTags) {\n    this.hreflangTags = hreflangTags;\n  }\n\n  element(element) {\n    if (this.hreflangTags) {\n      element.append(this.hreflangTags, { html: true });\n    }\n  }\n}\n\nexport default {\n  async fetch(request, env) {\n    const url = new URL(request.url);\n    const path = url.pathname;\n\n    // Le KV store contient des entrées comme :\n    // key: \"/fr/produits/chaise-bureau\"\n    // value: [{\"lang\":\"en\",\"href\":\"https://shop.example.com/en/products/office-chair\"},\n    //         {\"lang\":\"de\",\"href\":\"https://shop.example.com/de/produkte/burostuhl\"},\n    //         {\"lang\":\"fr\",\"href\":\"https://shop.example.com/fr/produits/chaise-bureau\"}]\n    const mappingRaw = await env.HREFLANG_MAP.get(path);\n\n    const response = await fetch(request);\n    const contentType = response.headers.get('content-type') || '';\n\n    if (!contentType.includes('text/html') || !mappingRaw) {\n      return response;\n    }\n\n    const mapping = JSON.parse(mappingRaw);\n    const hreflangHtml = mapping\n      .map(entry => `\u003Clink rel=\"alternate\" hreflang=\"${entry.lang}\" href=\"${entry.href}\" />`)\n      .join('\\n');\n\n    // Ajout du x-default (on utilise la version anglaise par convention)\n    const defaultEntry = mapping.find(e => e.lang === 'en');\n    const xDefault = defaultEntry\n      ? `\u003Clink rel=\"alternate\" hreflang=\"x-default\" href=\"${defaultEntry.href}\" />`\n      : '';\n\n    const fullHreflang = hreflangHtml + '\\n' + xDefault;\n\n    return new HTMLRewriter()\n      .on('link[rel=\"alternate\"][hreflang]', { element(el) { el.remove(); } })\n      .on('head', new HreflangInjector(fullHreflang))\n      .transform(response);\n  }\n};\n```\n\nLe KV store se met à jour via l'API Cloudflare ou via un script CI/CD qui parse un sitemap multilingue et génère le mapping. La latence de lecture du KV est inférieure à 10ms dans la même edge location.\n\n## Redirections massives sans toucher au serveur d'origine\n\nLes migrations de site génèrent des milliers de redirections. Les gérer dans le `.htaccess`, la config Nginx, ou — pire — dans le code applicatif, pose des problèmes de performance et de maintenabilité. Sur un fichier `.htaccess` avec 5 000 règles `RewriteRule`, chaque requête déclenche l'évaluation séquentielle de toutes les règles. Apache n'indexe pas les `RewriteRule`, c'est du pattern matching linéaire.\n\nÀ l'edge, vous utilisez un lookup dans un KV store ou un Worker qui charge une map de redirections. La complexité de lookup est O(1), pas O(n).\n\n### Scénario concret : migration de 15 000 URLs\n\nUn média en ligne avec 15 000 articles migre de WordPress vers un CMS headless. La structure d'URL change de `/YYYY/MM/slug` vers `/articles/slug`. L'équipe exporte la correspondance ancien → nouveau depuis la base WordPress.\n\n```javascript\n// worker-redirects.js\n// Redirections 301 massives via KV store\n\nexport default {\n  async fetch(request, env) {\n    const url = new URL(request.url);\n    const path = url.pathname;\n\n    // Lookup exact dans le KV store\n    // key: \"/2024/03/impact-ia-recherche\" → value: \"/articles/impact-ia-recherche\"\n    const newPath = await env.REDIRECT_MAP.get(path);\n\n    if (newPath) {\n      const destination = `${url.origin}${newPath}`;\n      return Response.redirect(destination, 301);\n    }\n\n    // Pattern-based fallback : anciennes URLs de catégories\n    const categoryMatch = path.match(/^\\/category\\/(.+)$/);\n    if (categoryMatch) {\n      const destination = `${url.origin}/rubriques/${categoryMatch[1]}`;\n      return Response.redirect(destination, 301);\n    }\n\n    // Pas de redirect : on laisse passer vers l'origin\n    return fetch(request);\n  }\n};\n```\n\nL'alimentation du KV store se fait en batch :\n\n```bash\n# Upload batch des redirections depuis un fichier JSON\n# redirects.json contient : [{\"key\":\"/2024/03/slug\",\"value\":\"/articles/slug\"}, ...]\nnpx wrangler kv:bulk put --binding=REDIRECT_MAP --namespace-id=abc123 redirects.json\n```\n\nLes résultats sur ce type de migration sont mesurables dans Google Search Console. Sur le cas d'un média comparable (chiffres observés en conditions réelles) : les 15 000 redirections sont crawlées par Googlebot en 4 à 7 jours, le rapport \"Couverture\" dans la Search Console montre la transition progressive des anciennes URLs (statut \"Page avec redirection\") vers les nouvelles (statut \"Valide\"). Le trafic organique retrouve son niveau pré-migration en 2 à 4 semaines si les redirections sont propres et les canonicals cohérents.\n\nUn point critique souvent négligé : monitorer que ces redirections restent en place après le déploiement. Un [déploiement mal testé](/blog/deploiement-vendredi-soir-comment-eviter-la-catastrophe-seo) peut écraser la configuration du Worker ou désactiver la route. Un outil de monitoring comme Seogard permet de détecter automatiquement si des URLs qui renvoyaient un 301 commencent à répondre en 404 ou en 200.\n\n## Lambda@Edge : les spécificités AWS\n\nSi votre infrastructure repose sur AWS et CloudFront, Lambda@Edge est l'alternative naturelle. L'architecture est différente : au lieu d'un fichier unique déployé globalement, vous attachez des fonctions Lambda à des behaviors CloudFront, avec quatre points d'interception.\n\n### Quel trigger pour quel usage SEO\n\n- **Viewer Request** : avant que CloudFront ne consulte son cache. Idéal pour les redirections (la réponse est renvoyée sans atteindre l'origin ni même le cache).\n- **Origin Request** : après le cache miss, avant l'appel à l'origin. Utile pour la réécriture d'URL côté serveur (A/B testing, pre-rendering conditionnel).\n- **Origin Response** : après la réponse de l'origin, avant la mise en cache. C'est ici que vous modifiez les headers HTTP (X-Robots-Tag, hreflang via header Link).\n- **Viewer Response** : après le cache hit/miss, avant l'envoi au client. Dernier point pour modifier les headers.\n\nPour l'injection de balises dans le body HTML, Lambda@Edge a une limitation importante : vous devez lire et modifier le body entier (pas de streaming HTMLRewriter comme chez Cloudflare). La taille du body est limitée à 1 MB pour les triggers viewer et 40 KB pour les réponses générées (sans appel à l'origin).\n\nCette contrainte rend Lambda@Edge moins adapté au body rewriting sur des pages volumineuses. En revanche, pour la manipulation de headers HTTP, c'est parfaitement fonctionnel.\n\n### Injection de X-Robots-Tag à l'edge\n\nUn cas d'usage fréquent : empêcher l'indexation de sections entières du site (pages de résultats de recherche interne, pages filtrées, espaces client) via le header `X-Robots-Tag` plutôt qu'une meta robots dans le HTML.\n\nL'avantage du header : il s'applique à tous les types de ressources (HTML, PDF, images), et il est traité par Googlebot avant même le parsing du HTML. La [documentation Google](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#xrobotstag) confirme la prise en charge complète du `X-Robots-Tag` dans les headers HTTP.\n\n```javascript\n// Lambda@Edge - Origin Response trigger\n// Injection de X-Robots-Tag sur les pages de recherche interne et les filtres\n\nexports.handler = async (event) => {\n  const response = event.Records[0].cf.response;\n  const request = event.Records[0].cf.request;\n  const uri = request.uri;\n  const querystring = request.querystring || '';\n\n  // Patterns à noindex\n  const noindexPatterns = [\n    /^\\/search/,           // Recherche interne\n    /^\\/catalog\\/.*\\?.*filter/,  // Pages filtrées\n    /^\\/account\\//,        // Espace client\n    /^\\/print\\//,          // Versions imprimables\n  ];\n\n  // Vérifier aussi les query strings problématiques\n  const noindexParams = ['q=', 'filter=', 'facet=', 'sort='];\n  const hasNoindexParam = noindexParams.some(param => querystring.includes(param));\n\n  const shouldNoindex = noindexPatterns.some(pattern => pattern.test(uri)) || hasNoindexParam;\n\n  if (shouldNoindex) {\n    response.headers['x-robots-tag'] = [{\n      key: 'X-Robots-Tag',\n      value: 'noindex, nofollow'\n    }];\n  }\n\n  return response;\n};\n```\n\nCette approche est complémentaire à la gestion des meta robots dans le HTML. Si votre application ajoute déjà un `\u003Cmeta name=\"robots\" content=\"noindex\">`, le header `X-Robots-Tag` sert de filet de sécurité en cas de régression côté front. C'est le type de divergence que l'on observe régulièrement entre [SSR et CSR](/blog/comparer-ssr-et-csr-detecter-les-divergences-invisibles) : le serveur envoie le bon header, mais le JavaScript côté client écrase la meta.\n\n## Pre-rendering sélectif à l'edge\n\nLe pre-rendering (ou dynamic rendering) consiste à servir une version HTML pré-générée aux crawlers, et la version JavaScript standard aux utilisateurs. Google [déconseille le cloaking](https://developers.google.com/search/docs/crawling-indexing/cloaking) mais considère le dynamic rendering comme une solution acceptable temporaire pour les sites qui ne peuvent pas implémenter le SSR.\n\nL'edge est l'endroit logique pour cette détection. Plutôt que de le faire dans votre application (ce qui ajoute de la complexité), le Worker détecte le user-agent et route vers un service de pre-rendering.\n\n```javascript\n// worker-prerender.js\n// Routage conditionnel vers un service de pre-rendering\n\nconst BOT_USER_AGENTS = [\n  'googlebot',\n  'bingbot',\n  'slurp',\n  'duckduckbot',\n  'baiduspider',\n  'yandexbot',\n  'facebot',\n  'twitterbot',\n  'linkedinbot',\n  'applebot',\n  'gptbot',\n  'chatgpt-user',\n  'claudebot',\n  'anthropic-ai',\n];\n\nconst PRERENDER_SERVICE = 'https://prerender.votredomaine.com';\n\n// Chemins à ne PAS pre-render (assets, API, etc.)\nconst BYPASS_PATTERNS = [\n  /\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|mp4|webp|avif)$/i,\n  /^\\/api\\//,\n  /^\\/ws\\//,\n  /^\\/_next\\//,\n];\n\nfunction isBot(userAgent) {\n  const ua = (userAgent || '').toLowerCase();\n  return BOT_USER_AGENTS.some(bot => ua.includes(bot));\n}\n\nexport default {\n  async fetch(request) {\n    const url = new URL(request.url);\n    const userAgent = request.headers.get('user-agent') || '';\n\n    // Bypass pour les assets et les API\n    if (BYPASS_PATTERNS.some(pattern => pattern.test(url.pathname))) {\n      return fetch(request);\n    }\n\n    if (isBot(userAgent)) {\n      // Appel au service de pre-rendering\n      const prerenderUrl = `${PRERENDER_SERVICE}${url.pathname}${url.search}`;\n\n      try {\n        const prerenderResponse = await fetch(prerenderUrl, {\n          headers: {\n            'X-Original-Host': url.hostname,\n            'X-Original-Proto': url.protocol.replace(':', ''),\n          },\n          cf: { cacheTtl: 86400 }, // Cache le résultat pre-rendu 24h à l'edge\n        });\n\n        if (prerenderResponse.ok) {\n          const response = new Response(prerenderResponse.body, prerenderResponse);\n          response.headers.set('X-Prerendered', 'true');\n          return response;\n        }\n      } catch (e) {\n        // Fallback vers l'origin en cas d'erreur du prerender service\n      }\n    }\n\n    return fetch(request);\n  }\n};\n```\n\nUn point important : la liste des user-agents bots s'allonge rapidement. En 2026, les crawlers d'IA (GPTBot, ClaudeBot, etc.) représentent un volume significatif de trafic bot, [parfois supérieur à Googlebot](/blog/chatgpt-now-crawls-3-6x-more-than-googlebot-what-24m-requests-reveal). Votre logique de détection doit être maintenue activement.\n\nLe trade-off du pre-rendering à l'edge : vous ajoutez une dépendance à un service tiers (ou auto-hébergé) qui doit rester disponible et performant. Si le service de pre-rendering tombe, le fallback vers l'origin doit fonctionner sans disruption. Testez ce fallback régulièrement.\n\n## Vérification et debugging : la partie que tout le monde néglige\n\nDéployer un Worker, c'est facile. S'assurer qu'il fait bien ce qu'il est censé faire sur 28 000 URLs, sur la durée, c'est autre chose.\n\n### Vérifier les modifications à l'edge avec les bons outils\n\n**curl avec les headers complets** — la première ligne de défense :\n\n```bash\n# Vérifier les headers de réponse (X-Robots-Tag, canonical, etc.)\ncurl -sI -H \"User-Agent: Googlebot\" \"https://shop.votredomaine.com/catalog/chaises?sort=price\" | grep -i \"x-robots\\|link\\|location\"\n\n# Vérifier le body HTML transformé\ncurl -s -H \"User-Agent: Googlebot\" \"https://shop.votredomaine.com/fr/produits/chaise-bureau\" | grep -i \"hreflang\\|canonical\"\n\n# Comparer la réponse bot vs utilisateur\ndiff \u003C(curl -s \"https://shop.votredomaine.com/page\" | head -50) \\\n     \u003C(curl -s -H \"User-Agent: Googlebot\" \"https://shop.votredomaine.com/page\" | head -50)\n```\n\n**Chrome DevTools** : l'onglet Network avec l'override du user-agent permet de vérifier ce que reçoit un crawler. L'option \"Disable cache\" est indispensable — sinon vous voyez la version cachée par votre navigateur, pas la réponse edge. Pour une utilisation avancée de DevTools dans un contexte SEO, consultez [ce guide](/blog/chrome-devtools-pour-le-seo-astuces-avancees).\n\n**Screaming Frog** : configurez un custom user-agent Googlebot et crawlez un échantillon représentatif (500-1000 URLs par section). Vérifiez dans l'onglet \"Directives\" que les canonicals injectées sont bien présentes, et dans \"Hreflang\" que le mapping est cohérent. Screaming Frog parse le HTML final reçu, donc il voit bien les modifications faites à l'edge.\n\n**Google Search Console** : l'outil d'inspection d'URL montre le HTML tel que Googlebot le voit. C'est la validation ultime. Mais attention : l'inspection utilise le crawler mobile de Google, pas un crawl depuis une edge location spécifique. Si votre Worker a un bug conditionnel (par exemple, il ne se déclenche que sur certaines edge locations), l'inspection Search Console pourrait donner un résultat correct alors que d'autres edge locations servent un HTML non modifié. Les [rapports souvent ignorés de la Search Console](/blog/google-search-console-les-rapports-que-vous-ignorez) peuvent révéler ce type d'incohérence.\n\n### Le piège du cache\n\nVotre CDN cache les réponses. Si le Worker modifie la réponse en fonction du user-agent (pre-rendering conditionnel), vous devez inclure le user-agent dans la clé de cache. Sinon, le premier visiteur (humain ou bot) détermine ce que tout le monde reçoit jusqu'à l'expiration du cache.\n\nSur Cloudflare, cela se gère via le header `Vary: User-Agent` ou, plus finement, via la Cache API du Worker pour créer des clés de cache distinctes. Mais attention : `Vary: User-Agent` multiplie les entrées de cache et réduit drastiquement le hit ratio. La solution propre est de varier sur un signal binaire (bot vs non-bot), pas sur le user-agent complet.\n\n```javascript\n// Cache key distincte bot vs non-bot\nconst cacheKey = new Request(request.url + (isBot(userAgent) ? '?_bot=1' : ''), request);\n```\n\n## Les limites et les risques de l'Edge SEO\n\nL'Edge SEO n'est pas une solution universelle. Quelques garde-fous.\n\n### Le risque de dette technique invisible\n\nChaque Worker est une couche de logique qui vit en dehors de votre codebase applicatif. Si la personne qui l'a écrit quitte l'équipe et que le Worker n'est pas documenté, vous avez une bombe à retardement. Six mois plus tard, un développeur refactorise le front et ajoute des canonicals côté application — sans savoir qu'un Worker les réécrit à l'edge. Résultat : des conflits de directives impossibles à diagnostiquer sans connaître l'existence du Worker.\n\nLa règle : chaque Worker SEO doit être versionné dans le même repo que le reste de l'infrastructure, documenté dans un registre d'edge rules accessible à toute l'équipe, et couvert par des tests automatisés. L'[intégration de checks SEO dans le CI/CD](/blog/automatiser-les-checks-seo-dans-le-ci-cd) est le bon framework pour ça.\n\n### Les limites de compute\n\nLes Cloudflare Workers ont un budget CPU strict. Un HTMLRewriter qui parse des pages de 500 KB fonctionne bien. Un Worker qui fait 10 appels KV, parse le HTML, injecte du JSON-LD calculé dynamiquement et applique des regex complexes sur le body peut dépasser les 50ms de CPU. Sur des volumes de trafic élevés (1M+ requêtes/jour), le coût des Workers peut aussi devenir significatif.\n\nLambda@Edge a des contraintes plus strictes sur la taille du body (1 MB max) et le nombre de requêtes réseau sortantes. Pas de VPC access, pas de librairies lourdes. Votre code doit rester léger.\n\n### Quand NE PAS utiliser l'Edge SEO\n\n- **Comme substitut permanent au fix côté application.** L'edge doit être un patch rapide ou une couche d'enrichissement, pas une architecture permanente qui masque des problèmes applicatifs.\n- **Pour des transformations HTML complexes** (restructuration du DOM, déplacement de blocs entiers). Le HTMLRewriter de Cloudflare est puissant mais ce n'est pas un DOM parser complet. Les manipulations complexes sont fragiles et difficiles à maintenir.\n- **Sur des pages dynamiques avec personnalisation lourde.** Si chaque utilisateur voit un contenu différent et que le cache est désactivé, le Worker s'exécute à chaque requête. Le coût et la latence ajoutée deviennent problématiques.\n\n## Monitoring continu des modifications edge\n\nLe vrai risque de l'Edge SEO est silencieux : un Worker qui cesse de fonctionner correctement ne génère pas d'erreur visible. Le site continue de servir des pages — juste sans les bonnes balises SEO. Si votre seule méthode de vérification est un crawl Screaming Frog mensuel, vous pouvez perdre des semaines avant de détecter la régression.\n\nC'est exactement le type de problème que les [audits ponctuels ne détectent pas](/blog/monitoring-seo-pourquoi-les-audits-ponctuels-ne-suffisent-plus). Un monitoring continu — qui vérifie quotidiennement la présence et la cohérence des balises canonical, hreflang, et des headers HTTP — est indispensable dès que vous faites du SEO à l'edge. Seogard est conçu pour détecter ces régressions automatiquement, y compris la disparition d'un header `X-Robots-Tag` ou d'une balise canonical qui était injectée par un Worker.\n\nLa combinaison Edge SEO + monitoring continu est ce qui sépare les équipes qui subissent les régressions de celles qui les corrigent avant que le trafic ne chute. Les Workers sont un outil puissant — à condition de ne jamais les considérer comme du \"fire and forget\".","https://seogard.io/blog/edge-seo-modifier-les-reponses-http-au-niveau-cdn","Edge SEO","2026-04-09T02:02:35.967Z","2026-04-09","Cloudflare Workers et Lambda@Edge pour injecter des balises, gérer des redirections et corriger le SEO technique sans toucher au code source.","\u003Cp>Un site e-commerce de 28 000 pages migre de plateforme. L'équipe marketing découvre 3 semaines après la mise en production que 4 200 pages ont perdu leur balise canonical, que les hreflang ont disparu sur l'ensemble du répertoire \u003Ccode>/fr/\u003C/code>, et que 1 600 anciennes URLs renvoient un 200 au lieu d'un 301. Le backlog de la dev team est plein pour 6 sprints. Le trafic organique chute de 18% en 10 jours. L'Edge SEO aurait permis de corriger tout ça en quelques heures, sans toucher à une seule ligne de code applicatif.\u003C/p>\n\u003Ch2>Ce que l'Edge SEO change fondamentalement\u003C/h2>\n\u003Cp>L'Edge SEO consiste à intercepter et modifier les réponses HTTP au niveau du CDN — entre le serveur d'origine et le navigateur (ou le crawler). Concrètement, le code s'exécute sur les points de présence (PoPs) du CDN, pas sur votre infrastructure applicative.\u003C/p>\n\u003Cp>Cette approche résout un problème organisationnel autant que technique : dans la majorité des organisations, les équipes SEO n'ont pas accès au code source en production. Chaque modification passe par un ticket, une priorisation sprint, une review, un déploiement. Ce cycle peut prendre des semaines pour une balise canonical.\u003C/p>\n\u003Ch3>Les deux plateformes dominantes\u003C/h3>\n\u003Cp>\u003Cstrong>Cloudflare Workers\u003C/strong> : code JavaScript/TypeScript exécuté sur le réseau Cloudflare (300+ PoPs). Basé sur le runtime V8 isolates, pas sur Node.js. Le temps de cold start est inférieur à 5ms dans la plupart des cas. La documentation officielle détaille les limites d'exécution : 10ms de CPU time sur le plan gratuit, 50ms sur le plan payant (\u003Ca href=\"https://developers.cloudflare.com/workers/platform/limits/\">Cloudflare Workers Limits\u003C/a>).\u003C/p>\n\u003Cp>\u003Cstrong>Lambda@Edge\u003C/strong> : fonctions Lambda déployées sur les edge locations CloudFront d'AWS. Quatre points d'interception : viewer request, origin request, origin response, viewer response. Le cold start est plus élevé (parfois 50-200ms), mais la puissance de calcul disponible est supérieure. La \u003Ca href=\"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html\">documentation AWS\u003C/a> spécifie un timeout de 5 secondes pour les events viewer et 30 secondes pour les events origin.\u003C/p>\n\u003Cp>Le choix entre les deux dépend de votre stack existante. Si votre CDN est déjà CloudFront, Lambda@Edge évite une migration. Si vous êtes sur Cloudflare ou si vous partez de zéro, Workers offre une DX (Developer Experience) nettement supérieure : déploiement en secondes via \u003Ccode>wrangler\u003C/code>, logs en temps réel, et un modèle de pricing plus prévisible.\u003C/p>\n\u003Ch3>Ce qui est modifiable à l'edge\u003C/h3>\n\u003Cp>La réponse HTTP est un flux que vous pouvez intercepter entièrement. Vous pouvez :\u003C/p>\n\u003Cul>\n\u003Cli>Ajouter, modifier ou supprimer des headers HTTP (X-Robots-Tag, Link, Hreflang via headers)\u003C/li>\n\u003Cli>Réécrire le body HTML (injection de balises \u003Ccode>&#x3C;link>\u003C/code>, \u003Ccode>&#x3C;meta>\u003C/code>, scripts structured data)\u003C/li>\n\u003Cli>Implémenter des redirections (301, 302, 308) sans toucher au serveur d'origine\u003C/li>\n\u003Cli>Servir un body complètement différent selon le user-agent (pre-rendering sélectif)\u003C/li>\n\u003Cli>Modifier le status code de la réponse\u003C/li>\n\u003C/ul>\n\u003Cp>Le body rewriting via l'API \u003Ccode>HTMLRewriter\u003C/code> de Cloudflare est la fonctionnalité la plus puissante pour le SEO. Elle permet de parser et modifier le HTML en streaming, sans charger tout le document en mémoire.\u003C/p>\n\u003Ch2>Injection de balises SEO avec Cloudflare Workers\u003C/h2>\n\u003Cp>Le cas d'usage le plus fréquent : injecter des canonicals, des hreflang, ou du JSON-LD sur des milliers de pages, piloté par un fichier de configuration externe.\u003C/p>\n\u003Ch3>Injecter des canonical dynamiques\u003C/h3>\n\u003Cp>Prenons un scénario réaliste : votre plateforme e-commerce génère des URLs avec des paramètres de tri et de filtrage (\u003Ccode>?sort=price&#x26;color=red\u003C/code>). Le CMS ne gère pas correctement les canonicals sur ces variantes. Plutôt que d'attendre un fix côté back-end, vous déployez un Worker.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// worker-canonical.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Cloudflare Worker : injection de canonical auto-référençant (strip des query params)\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\"> PARAMS_TO_STRIP\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sort'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'order'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'color'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'size'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'page'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'ref'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'utm_source'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'utm_medium'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'utm_campaign'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">];\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">class\u003C/span>\u003Cspan style=\"color:#B392F0\"> CanonicalHandler\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  constructor\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">canonicalUrl\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.canonicalUrl \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> canonicalUrl;\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:#B392F0\">  element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Supprime toute canonical existante pour éviter les doublons\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (element.tagName \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'link'\u003C/span>\u003Cspan style=\"color:#F97583\"> &#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> element.\u003C/span>\u003Cspan style=\"color:#B392F0\">getAttribute\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'rel'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'canonical'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      element.\u003C/span>\u003Cspan style=\"color:#B392F0\">remove\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\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">class\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadHandler\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  constructor\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">canonicalUrl\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.canonicalUrl \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> canonicalUrl;\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:#B392F0\">  element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    element.\u003C/span>\u003Cspan style=\"color:#B392F0\">append\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`&#x3C;link rel=\"canonical\" href=\"${\u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">canonicalUrl\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\" />`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, { html: \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\">  }\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\"> getCanonicalUrl\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">url\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\"> parsed\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\">(url);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> params\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URLSearchParams\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(parsed.search);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  PARAMS_TO_STRIP\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">forEach\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">param\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> params.\u003C/span>\u003Cspan style=\"color:#B392F0\">delete\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(param));\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\"> cleanSearch\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> params.\u003C/span>\u003Cspan style=\"color:#B392F0\">toString\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\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">parsed\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">origin\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">parsed\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">pathname\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">cleanSearch\u003C/span>\u003Cspan style=\"color:#F97583\"> ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '?'\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> cleanSearch\u003C/span>\u003Cspan style=\"color:#F97583\"> :\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\">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>\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\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> contentType\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'content-type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Ne traiter que les réponses HTML\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\">contentType.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'text/html'\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:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> canonicalUrl\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCanonicalUrl\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:#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\">'link[rel=\"canonical\"]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> CanonicalHandler\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(canonicalUrl))\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\">'head'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> HeadHandler\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(canonicalUrl))\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>\u003C/code>\u003C/pre>\n\u003Cp>Ce Worker fait trois choses : il supprime toute balise canonical existante dans le \u003Ccode>&#x3C;head>\u003C/code>, il calcule l'URL canonique en strippant les paramètres indésirables, et il injecte la bonne canonical. Le tout en streaming — le TTFB ajouté est négligeable.\u003C/p>\n\u003Cp>Le déploiement est immédiat :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Déploiement via Wrangler CLI\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> wrangler\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> deploy\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> worker-canonical.js\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --name\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> seo-canonical-fix\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --route\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"shop.votredomaine.com/catalog/*\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La directive \u003Ccode>--route\u003C/code> restreint l'exécution aux URLs du catalogue. C'est critique : un Worker qui s'exécute sur toutes les requêtes (y compris les assets statiques) consomme du CPU inutilement et peut impacter les coûts.\u003C/p>\n\u003Ch3>Injection de hreflang à l'échelle\u003C/h3>\n\u003Cp>Les hreflang sont un cauchemar récurrent sur les sites multilingues. Le mapping entre les versions linguistiques change, des pages sont dépubliées dans certaines langues, et les balises deviennent incohérentes. Un problème que l'on retrouve fréquemment dans les \u003Ca href=\"/blog/regressions-seo-les-10-types-les-plus-frequents\">régressions SEO classiques\u003C/a>.\u003C/p>\n\u003Cp>La solution edge : un Worker qui lit un mapping depuis un KV store (key-value store intégré à Cloudflare) et injecte les hreflang correspondants.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// worker-hreflang.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Lecture du mapping hreflang depuis Cloudflare KV\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">class\u003C/span>\u003Cspan style=\"color:#B392F0\"> HreflangInjector\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  constructor\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">hreflangTags\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.hreflangTags \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hreflangTags;\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:#B392F0\">  element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">element\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\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.hreflangTags) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      element.\u003C/span>\u003Cspan style=\"color:#B392F0\">append\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">this\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.hreflangTags, { html: \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\">    }\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\">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\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> path\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url.pathname;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Le KV store contient des entrées comme :\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // key: \"/fr/produits/chaise-bureau\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // value: [{\"lang\":\"en\",\"href\":\"https://shop.example.com/en/products/office-chair\"},\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    //         {\"lang\":\"de\",\"href\":\"https://shop.example.com/de/produkte/burostuhl\"},\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    //         {\"lang\":\"fr\",\"href\":\"https://shop.example.com/fr/produits/chaise-bureau\"}]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> mappingRaw\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">HREFLANG_MAP\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(path);\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\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> contentType\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'content-type'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\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\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">contentType.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'text/html'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#F97583\"> !\u003C/span>\u003Cspan style=\"color:#E1E4E8\">mappingRaw) {\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\"> mapping\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">parse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(mappingRaw);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> hreflangHtml\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> mapping\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      .\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">entry\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `&#x3C;link rel=\"alternate\" hreflang=\"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">entry\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">lang\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\" href=\"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">entry\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">href\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>\u003Cspan style=\"color:#B392F0\">join\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\n\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Ajout du x-default (on utilise la version anglaise par convention)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> defaultEntry\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> mapping.\u003C/span>\u003Cspan style=\"color:#B392F0\">find\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">e\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> e.lang \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'en'\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\"> xDefault\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> defaultEntry\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      ?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `&#x3C;link rel=\"alternate\" hreflang=\"x-default\" href=\"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">defaultEntry\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">href\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\" />`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\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\"> fullHreflang\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hreflangHtml \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '\u003C/span>\u003Cspan style=\"color:#79B8FF\">\\n\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> xDefault;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">'link[rel=\"alternate\"][hreflang]'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, { \u003C/span>\u003Cspan style=\"color:#B392F0\">element\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">el\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) { el.\u003C/span>\u003Cspan style=\"color:#B392F0\">remove\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\">'head'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> HreflangInjector\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(fullHreflang))\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>\u003C/code>\u003C/pre>\n\u003Cp>Le KV store se met à jour via l'API Cloudflare ou via un script CI/CD qui parse un sitemap multilingue et génère le mapping. La latence de lecture du KV est inférieure à 10ms dans la même edge location.\u003C/p>\n\u003Ch2>Redirections massives sans toucher au serveur d'origine\u003C/h2>\n\u003Cp>Les migrations de site génèrent des milliers de redirections. Les gérer dans le \u003Ccode>.htaccess\u003C/code>, la config Nginx, ou — pire — dans le code applicatif, pose des problèmes de performance et de maintenabilité. Sur un fichier \u003Ccode>.htaccess\u003C/code> avec 5 000 règles \u003Ccode>RewriteRule\u003C/code>, chaque requête déclenche l'évaluation séquentielle de toutes les règles. Apache n'indexe pas les \u003Ccode>RewriteRule\u003C/code>, c'est du pattern matching linéaire.\u003C/p>\n\u003Cp>À l'edge, vous utilisez un lookup dans un KV store ou un Worker qui charge une map de redirections. La complexité de lookup est O(1), pas O(n).\u003C/p>\n\u003Ch3>Scénario concret : migration de 15 000 URLs\u003C/h3>\n\u003Cp>Un média en ligne avec 15 000 articles migre de WordPress vers un CMS headless. La structure d'URL change de \u003Ccode>/YYYY/MM/slug\u003C/code> vers \u003Ccode>/articles/slug\u003C/code>. L'équipe exporte la correspondance ancien → nouveau depuis la base WordPress.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// worker-redirects.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Redirections 301 massives via KV store\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\"> 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\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> path\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> url.pathname;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Lookup exact dans le KV store\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // key: \"/2024/03/impact-ia-recherche\" → value: \"/articles/impact-ia-recherche\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> newPath\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">REDIRECT_MAP\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(path);\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\"> (newPath) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> destination\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">origin\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">newPath\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:#E1E4E8\"> Response.\u003C/span>\u003Cspan style=\"color:#B392F0\">redirect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(destination, \u003C/span>\u003Cspan style=\"color:#79B8FF\">301\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\">    // Pattern-based fallback : anciennes URLs de catégories\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> categoryMatch\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> path.\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\">category\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#DBEDFF\">(\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#DBEDFF\">)\u003C/span>\u003Cspan style=\"color:#F97583\">$\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\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\"> (categoryMatch) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> destination\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">origin\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/rubriques/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">categoryMatch\u003C/span>\u003Cspan style=\"color:#9ECBFF\">[\u003C/span>\u003Cspan style=\"color:#79B8FF\">1\u003C/span>\u003Cspan style=\"color:#9ECBFF\">]\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:#E1E4E8\"> Response.\u003C/span>\u003Cspan style=\"color:#B392F0\">redirect\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(destination, \u003C/span>\u003Cspan style=\"color:#79B8FF\">301\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\">    // Pas de redirect : on laisse passer vers l'origin\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\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>L'alimentation du KV store se fait en batch :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Upload batch des redirections depuis un fichier JSON\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># redirects.json contient : [{\"key\":\"/2024/03/slug\",\"value\":\"/articles/slug\"}, ...]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">npx\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> wrangler\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> kv:bulk\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> put\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --binding=REDIRECT_MAP\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --namespace-id=abc123\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> redirects.json\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Les résultats sur ce type de migration sont mesurables dans Google Search Console. Sur le cas d'un média comparable (chiffres observés en conditions réelles) : les 15 000 redirections sont crawlées par Googlebot en 4 à 7 jours, le rapport \"Couverture\" dans la Search Console montre la transition progressive des anciennes URLs (statut \"Page avec redirection\") vers les nouvelles (statut \"Valide\"). Le trafic organique retrouve son niveau pré-migration en 2 à 4 semaines si les redirections sont propres et les canonicals cohérents.\u003C/p>\n\u003Cp>Un point critique souvent négligé : monitorer que ces redirections restent en place après le déploiement. Un \u003Ca href=\"/blog/deploiement-vendredi-soir-comment-eviter-la-catastrophe-seo\">déploiement mal testé\u003C/a> peut écraser la configuration du Worker ou désactiver la route. Un outil de monitoring comme Seogard permet de détecter automatiquement si des URLs qui renvoyaient un 301 commencent à répondre en 404 ou en 200.\u003C/p>\n\u003Ch2>Lambda@Edge : les spécificités AWS\u003C/h2>\n\u003Cp>Si votre infrastructure repose sur AWS et CloudFront, Lambda@Edge est l'alternative naturelle. L'architecture est différente : au lieu d'un fichier unique déployé globalement, vous attachez des fonctions Lambda à des behaviors CloudFront, avec quatre points d'interception.\u003C/p>\n\u003Ch3>Quel trigger pour quel usage SEO\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Viewer Request\u003C/strong> : avant que CloudFront ne consulte son cache. Idéal pour les redirections (la réponse est renvoyée sans atteindre l'origin ni même le cache).\u003C/li>\n\u003Cli>\u003Cstrong>Origin Request\u003C/strong> : après le cache miss, avant l'appel à l'origin. Utile pour la réécriture d'URL côté serveur (A/B testing, pre-rendering conditionnel).\u003C/li>\n\u003Cli>\u003Cstrong>Origin Response\u003C/strong> : après la réponse de l'origin, avant la mise en cache. C'est ici que vous modifiez les headers HTTP (X-Robots-Tag, hreflang via header Link).\u003C/li>\n\u003Cli>\u003Cstrong>Viewer Response\u003C/strong> : après le cache hit/miss, avant l'envoi au client. Dernier point pour modifier les headers.\u003C/li>\n\u003C/ul>\n\u003Cp>Pour l'injection de balises dans le body HTML, Lambda@Edge a une limitation importante : vous devez lire et modifier le body entier (pas de streaming HTMLRewriter comme chez Cloudflare). La taille du body est limitée à 1 MB pour les triggers viewer et 40 KB pour les réponses générées (sans appel à l'origin).\u003C/p>\n\u003Cp>Cette contrainte rend Lambda@Edge moins adapté au body rewriting sur des pages volumineuses. En revanche, pour la manipulation de headers HTTP, c'est parfaitement fonctionnel.\u003C/p>\n\u003Ch3>Injection de X-Robots-Tag à l'edge\u003C/h3>\n\u003Cp>Un cas d'usage fréquent : empêcher l'indexation de sections entières du site (pages de résultats de recherche interne, pages filtrées, espaces client) via le header \u003Ccode>X-Robots-Tag\u003C/code> plutôt qu'une meta robots dans le HTML.\u003C/p>\n\u003Cp>L'avantage du header : il s'applique à tous les types de ressources (HTML, PDF, images), et il est traité par Googlebot avant même le parsing du HTML. La \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#xrobotstag\">documentation Google\u003C/a> confirme la prise en charge complète du \u003Ccode>X-Robots-Tag\u003C/code> dans les headers HTTP.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Lambda@Edge - Origin Response trigger\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Injection de X-Robots-Tag sur les pages de recherche interne et les filtres\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">exports\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">handler\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#FFAB70\">event\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\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\"> event.Records[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].cf.response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> request\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> event.Records[\u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">].cf.request;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> uri\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.uri;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> querystring\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.querystring \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  // Patterns à noindex\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> noindexPatterns\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">search\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,           \u003C/span>\u003Cspan style=\"color:#6A737D\">// Recherche interne\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">catalog\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\?\u003C/span>\u003Cspan style=\"color:#79B8FF\">.\u003C/span>\u003Cspan style=\"color:#F97583\">*\u003C/span>\u003Cspan style=\"color:#DBEDFF\">filter\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,  \u003C/span>\u003Cspan style=\"color:#6A737D\">// Pages filtrées\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">account\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,        \u003C/span>\u003Cspan style=\"color:#6A737D\">// Espace client\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">print\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,          \u003C/span>\u003Cspan style=\"color:#6A737D\">// Versions imprimables\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\">  // Vérifier aussi les query strings problématiques\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> noindexParams\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'q='\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'filter='\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'facet='\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'sort='\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\"> hasNoindexParam\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> noindexParams.\u003C/span>\u003Cspan style=\"color:#B392F0\">some\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">param\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> querystring.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(param));\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\"> shouldNoindex\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> noindexPatterns.\u003C/span>\u003Cspan style=\"color:#B392F0\">some\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">pattern\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pattern.\u003C/span>\u003Cspan style=\"color:#B392F0\">test\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(uri)) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> hasNoindexParam;\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\"> (shouldNoindex) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.headers[\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'x-robots-tag'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">] \u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      key: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'X-Robots-Tag'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      value: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'noindex, nofollow'\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\">  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>Cette approche est complémentaire à la gestion des meta robots dans le HTML. Si votre application ajoute déjà un \u003Ccode>&#x3C;meta name=\"robots\" content=\"noindex\">\u003C/code>, le header \u003Ccode>X-Robots-Tag\u003C/code> sert de filet de sécurité en cas de régression côté front. C'est le type de divergence que l'on observe régulièrement entre \u003Ca href=\"/blog/comparer-ssr-et-csr-detecter-les-divergences-invisibles\">SSR et CSR\u003C/a> : le serveur envoie le bon header, mais le JavaScript côté client écrase la meta.\u003C/p>\n\u003Ch2>Pre-rendering sélectif à l'edge\u003C/h2>\n\u003Cp>Le pre-rendering (ou dynamic rendering) consiste à servir une version HTML pré-générée aux crawlers, et la version JavaScript standard aux utilisateurs. Google \u003Ca href=\"https://developers.google.com/search/docs/crawling-indexing/cloaking\">déconseille le cloaking\u003C/a> mais considère le dynamic rendering comme une solution acceptable temporaire pour les sites qui ne peuvent pas implémenter le SSR.\u003C/p>\n\u003Cp>L'edge est l'endroit logique pour cette détection. Plutôt que de le faire dans votre application (ce qui ajoute de la complexité), le Worker détecte le user-agent et route vers un service de pre-rendering.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// worker-prerender.js\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Routage conditionnel vers un service de pre-rendering\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\"> BOT_USER_AGENTS\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'googlebot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'bingbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'slurp'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'duckduckbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'baiduspider'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'yandexbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'facebot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'twitterbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'linkedinbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'applebot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'gptbot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'chatgpt-user'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'claudebot'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  'anthropic-ai'\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\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> PRERENDER_SERVICE\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://prerender.votredomaine.com'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Chemins à ne PAS pre-render (assets, API, etc.)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> BYPASS_PATTERNS\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  /\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\.\u003C/span>\u003Cspan style=\"color:#DBEDFF\">(js\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">css\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">png\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">jpg\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">jpeg\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">gif\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">svg\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">ico\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">woff\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">woff2\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">ttf\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">eot\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">mp4\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">webp\u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#DBEDFF\">avif)\u003C/span>\u003Cspan style=\"color:#F97583\">$\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#F97583\">i\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">api\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">ws\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\u003C/span>\u003Cspan style=\"color:#9ECBFF\">/\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">_next\u003C/span>\u003Cspan style=\"color:#85E89D;font-weight:bold\">\\/\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\">function\u003C/span>\u003Cspan style=\"color:#B392F0\"> isBot\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">userAgent\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\"> ua\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (userAgent \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">).\u003C/span>\u003Cspan style=\"color:#B392F0\">toLowerCase\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\"> BOT_USER_AGENTS\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">some\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">bot\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ua.\u003C/span>\u003Cspan style=\"color:#B392F0\">includes\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(bot));\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\">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>\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\">\u003Cspan style=\"color:#F97583\">    const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> userAgent\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> request.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'user-agent'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Bypass pour les assets et les API\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#79B8FF\">BYPASS_PATTERNS\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">some\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">pattern\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> pattern.\u003C/span>\u003Cspan style=\"color:#B392F0\">test\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(url.pathname))) {\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\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#B392F0\">isBot\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(userAgent)) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // Appel au service de pre-rendering\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> prerenderUrl\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> `${\u003C/span>\u003Cspan style=\"color:#79B8FF\">PRERENDER_SERVICE\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">pathname\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">url\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">search\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      try\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\"> prerenderResponse\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\">(prerenderUrl, {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          headers: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'X-Original-Host'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: url.hostname,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            'X-Original-Proto'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: url.protocol.\u003C/span>\u003Cspan style=\"color:#B392F0\">replace\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">':'\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:#E1E4E8\">          },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          cf: { cacheTtl: \u003C/span>\u003Cspan style=\"color:#79B8FF\">86400\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }, \u003C/span>\u003Cspan style=\"color:#6A737D\">// Cache le résultat pre-rendu 24h à l'edge\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\">        if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (prerenderResponse.ok) {\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:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Response\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(prerenderResponse.body, prerenderResponse);\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-Prerendered'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'true'\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\">\u003Cspan style=\"color:#E1E4E8\">      } \u003C/span>\u003Cspan style=\"color:#F97583\">catch\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (e) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        // Fallback vers l'origin en cas d'erreur du prerender service\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\">    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\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Un point important : la liste des user-agents bots s'allonge rapidement. En 2026, les crawlers d'IA (GPTBot, ClaudeBot, etc.) représentent un volume significatif de trafic bot, \u003Ca href=\"/blog/chatgpt-now-crawls-3-6x-more-than-googlebot-what-24m-requests-reveal\">parfois supérieur à Googlebot\u003C/a>. Votre logique de détection doit être maintenue activement.\u003C/p>\n\u003Cp>Le trade-off du pre-rendering à l'edge : vous ajoutez une dépendance à un service tiers (ou auto-hébergé) qui doit rester disponible et performant. Si le service de pre-rendering tombe, le fallback vers l'origin doit fonctionner sans disruption. Testez ce fallback régulièrement.\u003C/p>\n\u003Ch2>Vérification et debugging : la partie que tout le monde néglige\u003C/h2>\n\u003Cp>Déployer un Worker, c'est facile. S'assurer qu'il fait bien ce qu'il est censé faire sur 28 000 URLs, sur la durée, c'est autre chose.\u003C/p>\n\u003Ch3>Vérifier les modifications à l'edge avec les bons outils\u003C/h3>\n\u003Cp>\u003Cstrong>curl avec les headers complets\u003C/strong> — la première ligne de défense :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier les headers de réponse (X-Robots-Tag, canonical, etc.)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -sI\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Googlebot\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://shop.votredomaine.com/catalog/chaises?sort=price\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"x-robots\\|link\\|location\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Vérifier le body HTML transformé\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\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Googlebot\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://shop.votredomaine.com/fr/produits/chaise-bureau\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"hreflang\\|canonical\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Comparer la réponse bot vs utilisateur\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">diff\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> &#x3C;(\u003C/span>\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"https://shop.votredomaine.com/page\" \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#B392F0\"> head\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -50\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">     &#x3C;(\u003C/span>\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Googlebot\" \"https://shop.votredomaine.com/page\" \u003C/span>\u003Cspan style=\"color:#F97583\">|\u003C/span>\u003Cspan style=\"color:#B392F0\"> head\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -50\u003C/span>\u003Cspan style=\"color:#9ECBFF\">)\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Cstrong>Chrome DevTools\u003C/strong> : l'onglet Network avec l'override du user-agent permet de vérifier ce que reçoit un crawler. L'option \"Disable cache\" est indispensable — sinon vous voyez la version cachée par votre navigateur, pas la réponse edge. Pour une utilisation avancée de DevTools dans un contexte SEO, consultez \u003Ca href=\"/blog/chrome-devtools-pour-le-seo-astuces-avancees\">ce guide\u003C/a>.\u003C/p>\n\u003Cp>\u003Cstrong>Screaming Frog\u003C/strong> : configurez un custom user-agent Googlebot et crawlez un échantillon représentatif (500-1000 URLs par section). Vérifiez dans l'onglet \"Directives\" que les canonicals injectées sont bien présentes, et dans \"Hreflang\" que le mapping est cohérent. Screaming Frog parse le HTML final reçu, donc il voit bien les modifications faites à l'edge.\u003C/p>\n\u003Cp>\u003Cstrong>Google Search Console\u003C/strong> : l'outil d'inspection d'URL montre le HTML tel que Googlebot le voit. C'est la validation ultime. Mais attention : l'inspection utilise le crawler mobile de Google, pas un crawl depuis une edge location spécifique. Si votre Worker a un bug conditionnel (par exemple, il ne se déclenche que sur certaines edge locations), l'inspection Search Console pourrait donner un résultat correct alors que d'autres edge locations servent un HTML non modifié. Les \u003Ca href=\"/blog/google-search-console-les-rapports-que-vous-ignorez\">rapports souvent ignorés de la Search Console\u003C/a> peuvent révéler ce type d'incohérence.\u003C/p>\n\u003Ch3>Le piège du cache\u003C/h3>\n\u003Cp>Votre CDN cache les réponses. Si le Worker modifie la réponse en fonction du user-agent (pre-rendering conditionnel), vous devez inclure le user-agent dans la clé de cache. Sinon, le premier visiteur (humain ou bot) détermine ce que tout le monde reçoit jusqu'à l'expiration du cache.\u003C/p>\n\u003Cp>Sur Cloudflare, cela se gère via le header \u003Ccode>Vary: User-Agent\u003C/code> ou, plus finement, via la Cache API du Worker pour créer des clés de cache distinctes. Mais attention : \u003Ccode>Vary: User-Agent\u003C/code> multiplie les entrées de cache et réduit drastiquement le hit ratio. La solution propre est de varier sur un signal binaire (bot vs non-bot), pas sur le user-agent complet.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// Cache key distincte bot vs non-bot\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> cacheKey\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Request\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(request.url \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#B392F0\">isBot\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(userAgent) \u003C/span>\u003Cspan style=\"color:#F97583\">?\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '?_bot=1'\u003C/span>\u003Cspan style=\"color:#F97583\"> :\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> ''\u003C/span>\u003Cspan style=\"color:#E1E4E8\">), request);\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Ch2>Les limites et les risques de l'Edge SEO\u003C/h2>\n\u003Cp>L'Edge SEO n'est pas une solution universelle. Quelques garde-fous.\u003C/p>\n\u003Ch3>Le risque de dette technique invisible\u003C/h3>\n\u003Cp>Chaque Worker est une couche de logique qui vit en dehors de votre codebase applicatif. Si la personne qui l'a écrit quitte l'équipe et que le Worker n'est pas documenté, vous avez une bombe à retardement. Six mois plus tard, un développeur refactorise le front et ajoute des canonicals côté application — sans savoir qu'un Worker les réécrit à l'edge. Résultat : des conflits de directives impossibles à diagnostiquer sans connaître l'existence du Worker.\u003C/p>\n\u003Cp>La règle : chaque Worker SEO doit être versionné dans le même repo que le reste de l'infrastructure, documenté dans un registre d'edge rules accessible à toute l'équipe, et couvert par des tests automatisés. L'\u003Ca href=\"/blog/automatiser-les-checks-seo-dans-le-ci-cd\">intégration de checks SEO dans le CI/CD\u003C/a> est le bon framework pour ça.\u003C/p>\n\u003Ch3>Les limites de compute\u003C/h3>\n\u003Cp>Les Cloudflare Workers ont un budget CPU strict. Un HTMLRewriter qui parse des pages de 500 KB fonctionne bien. Un Worker qui fait 10 appels KV, parse le HTML, injecte du JSON-LD calculé dynamiquement et applique des regex complexes sur le body peut dépasser les 50ms de CPU. Sur des volumes de trafic élevés (1M+ requêtes/jour), le coût des Workers peut aussi devenir significatif.\u003C/p>\n\u003Cp>Lambda@Edge a des contraintes plus strictes sur la taille du body (1 MB max) et le nombre de requêtes réseau sortantes. Pas de VPC access, pas de librairies lourdes. Votre code doit rester léger.\u003C/p>\n\u003Ch3>Quand NE PAS utiliser l'Edge SEO\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Comme substitut permanent au fix côté application.\u003C/strong> L'edge doit être un patch rapide ou une couche d'enrichissement, pas une architecture permanente qui masque des problèmes applicatifs.\u003C/li>\n\u003Cli>\u003Cstrong>Pour des transformations HTML complexes\u003C/strong> (restructuration du DOM, déplacement de blocs entiers). Le HTMLRewriter de Cloudflare est puissant mais ce n'est pas un DOM parser complet. Les manipulations complexes sont fragiles et difficiles à maintenir.\u003C/li>\n\u003Cli>\u003Cstrong>Sur des pages dynamiques avec personnalisation lourde.\u003C/strong> Si chaque utilisateur voit un contenu différent et que le cache est désactivé, le Worker s'exécute à chaque requête. Le coût et la latence ajoutée deviennent problématiques.\u003C/li>\n\u003C/ul>\n\u003Ch2>Monitoring continu des modifications edge\u003C/h2>\n\u003Cp>Le vrai risque de l'Edge SEO est silencieux : un Worker qui cesse de fonctionner correctement ne génère pas d'erreur visible. Le site continue de servir des pages — juste sans les bonnes balises SEO. Si votre seule méthode de vérification est un crawl Screaming Frog mensuel, vous pouvez perdre des semaines avant de détecter la régression.\u003C/p>\n\u003Cp>C'est exactement le type de problème que les \u003Ca href=\"/blog/monitoring-seo-pourquoi-les-audits-ponctuels-ne-suffisent-plus\">audits ponctuels ne détectent pas\u003C/a>. Un monitoring continu — qui vérifie quotidiennement la présence et la cohérence des balises canonical, hreflang, et des headers HTTP — est indispensable dès que vous faites du SEO à l'edge. Seogard est conçu pour détecter ces régressions automatiquement, y compris la disparition d'un header \u003Ccode>X-Robots-Tag\u003C/code> ou d'une balise canonical qui était injectée par un Worker.\u003C/p>\n\u003Cp>La combinaison Edge SEO + monitoring continu est ce qui sépare les équipes qui subissent les régressions de celles qui les corrigent avant que le trafic ne chute. Les Workers sont un outil puissant — à condition de ne jamais les considérer comme du \"fire and forget\".\u003C/p>",null,12,[18,19,20,21,22],"edge-seo","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)",[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},"69d740f8aa6b273b0c0f028c","a-b-testing-seo-tester-sans-penaliser-le-referencement","https://seogard.io/blog/a-b-testing-seo-tester-sans-penaliser-le-referencement","2026-04-09T06:02:32.665Z","Méthodes concrètes pour mener des tests A/B SEO conformes aux guidelines Google, sans cloaking ni régression de positionnement.",[33,34,35,36,18],"ab-testing","seo","cloaking","guidelines","A/B testing SEO : tester sans risquer la pénalité","Thu Apr 09 2026 06:02:32 GMT+0000 (Coordinated Universal Time)"]