[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fFPZ5wqSgP-6jsCTAN1lhovD1JeLS89AbbVAqUMn2ewk":3,"$fQpmicK9wgaSFufDVCo_SZdym56odG2uVJP7s_F46__k":24,"$fcTFjTaPPT1sVcnh1wWEGoTWMSaOD4bwmwntkMae6w5s":112},{"_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":22,"updatedAt":23},"6a2595ecaa6b273b0cf78369","astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant",0,"Equipe Seogard","# Sitemap Astro pointant vers build.local : 4 000 URLs envoyées dans le vide\n\nMercredi 11h. Le lead SEO d'une marketplace de mobilier design ouvre Search Console pour vérifier la couverture post-déploiement. Le rapport Sitemaps affiche un statut vert — \"Réussite\". Mais la colonne \"URL découvertes\" indique 0 pages indexées sur les 4 127 soumises. Il clique sur le sitemap. Chaque `\u003Cloc>` commence par `https://build.local/`. Le domaine de production n'apparaît nulle part. Depuis 11 jours, Google ingère un plan de site qui pointe vers un domaine qui n'existe pas.\n\n## Lundi 9h12 — Le silence avant l'alerte\n\nLe déploiement incriminé remonte au vendredi d'avant. L'équipe front a migré le site de Astro 4.x vers Astro 5, en profitant pour restructurer la configuration de build. Le pipeline CI/CD tourne sur GitHub Actions. Le site est hébergé sur Vercel. Tout passe au vert : build réussie, preview fonctionnelle, lighthouse score stable.\n\nPersonne ne regarde le sitemap.\n\nLundi matin, le lead SEO vérifie les positions sur un panel de 85 mots-clés prioritaires. Pas de mouvement anormal — Google n'a pas encore recrawlé massivement. Mais il note un détail : le dernier crawl du sitemap dans Search Console date de vendredi 17h23. Statut \"Réussite\", mais 0 URLs indexées depuis ce crawl.\n\nIl télécharge le fichier `sitemap-0.xml` directement depuis `https://www.meubles-design.example/sitemap-0.xml`. Le fichier se charge. Il fait 312 Ko. Il l'ouvre.\n\n```xml\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003Curlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  \u003Curl>\n    \u003Cloc>https://build.local/canapes/canape-angle-velours-bleu\u003C/loc>\n    \u003Clastmod>2026-05-29T14:22:00.000Z\u003C/lastmod>\n    \u003Cchangefreq>weekly\u003C/changefreq>\n    \u003Cpriority>0.7\u003C/priority>\n  \u003C/url>\n  \u003Curl>\n    \u003Cloc>https://build.local/tables/table-basse-chene-massif\u003C/loc>\n    \u003Clastmod>2026-05-29T14:22:00.000Z\u003C/lastmod>\n    \u003Cchangefreq>weekly\u003C/changefreq>\n    \u003Cpriority>0.7\u003C/priority>\n  \u003C/url>\n  \u003C!-- 4 125 autres entrées identiques -->\n\u003C/urlset>\n```\n\nChaque URL pointe vers `https://build.local`. Pas `https://www.meubles-design.example`.\n\nLe lead SEO envoie un message Slack à 9h34 : \"Le sitemap est cassé. Toutes les URLs pointent vers un domaine local. Depuis vendredi.\"\n\nL'équipe front vérifie d'abord le plugin `@astrojs/sitemap`. Il est bien installé, bien importé. Le build local génère le même fichier. Le dev principal pense d'abord à un bug du plugin — il ouvre une issue GitHub, cherche des rapports similaires. Rien de concluant.\n\nÀ 10h15, le CTO demande l'impact. Le lead SEO sort les chiffres de Search Console sur les 10 derniers jours : le taux de crawl des URLs de contenu a chuté de 340 requêtes/jour à 87. Google continue de crawler le site via les liens internes, mais il ignore le sitemap — logiquement, puisque les URLs `build.local` ne résolvent vers aucune IP. Le trafic organique n'a pas encore décroché visiblement, mais les nouvelles pages produit publiées depuis vendredi — 23 fiches — n'ont été indexées nulle part.\n\nÀ 10h42, le dev junior lâche dans le thread Slack : \"J'ai trouvé. C'est le `astro.config.mjs`.\"\n\n## Le bug : `site` configuré par environnement, mais l'environnement n'existe pas en CI\n\nLe problème est dans la propriété `site` du fichier `astro.config.mjs`. Cette propriété est [documentée par Astro](https://docs.astro.build/en/reference/configuration-reference/#site) comme la source de vérité pour générer les URLs absolues — dans le sitemap, dans les flux RSS, dans les balises canonical.\n\nVoici la configuration telle qu'elle existait avant la migration Astro 5 :\n\n```javascript\n// astro.config.mjs — version AVANT migration (Astro 4.x)\nimport { defineConfig } from 'astro/config';\nimport sitemap from '@astrojs/sitemap';\n\nexport default defineConfig({\n  site: 'https://www.meubles-design.example',\n  integrations: [sitemap()],\n});\n```\n\nDirect. Fonctionnel. Le domaine de production est en dur.\n\nLors de la migration vers Astro 5, l'équipe front a voulu \"professionnaliser\" la config. Le dev principal a introduit une gestion par variable d'environnement, pour distinguer les builds de développement, de staging et de production :\n\n```typescript\n// astro.config.mjs — version APRÈS migration (Astro 5)\nimport { defineConfig } from 'astro/config';\nimport sitemap from '@astrojs/sitemap';\n\nconst siteUrl = process.env.SITE_URL || 'https://build.local';\n\nexport default defineConfig({\n  site: siteUrl,\n  integrations: [\n    sitemap({\n      changefreq: 'weekly',\n      priority: 0.7,\n      lastmod: new Date(),\n    }),\n  ],\n  output: 'static',\n  build: {\n    format: 'directory',\n  },\n});\n```\n\nLa logique : en local, `SITE_URL` n'est pas défini, donc le fallback `https://build.local` s'applique. En CI, la variable devrait être injectée.\n\nLe problème : elle ne l'est pas.\n\nLe workflow GitHub Actions ressemble à ceci :\n\n```yaml\n# .github/workflows/deploy.yml\nname: Deploy to Vercel\non:\n  push:\n    branches: [main]\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - run: npm ci\n      - run: npm run build\n        env:\n          NODE_ENV: production\n          STRAPI_API_URL: ${{ secrets.STRAPI_API_URL }}\n          STRAPI_API_TOKEN: ${{ secrets.STRAPI_API_TOKEN }}\n          # SITE_URL manquant ici\n      - uses: amondnet/vercel-action@v25\n        with:\n          vercel-token: ${{ secrets.VERCEL_TOKEN }}\n          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}\n          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}\n```\n\n`SITE_URL` n'est jamais défini dans les secrets GitHub, ni dans les variables d'environnement du job de build. Le fallback `'https://build.local'` s'applique silencieusement. Astro ne lève aucun warning. Le build réussit. Le sitemap est généré. Vercel déploie le résultat.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nLe développeur ouvre le site sur `https://www.meubles-design.example/canapes/canape-angle-velours-bleu`. La page se charge. Le HTML est correct. Les balises meta sont en place. Le canonical pointe vers la bonne URL — parce que l'équipe utilise `Astro.url` dans le layout, qui se base sur l'URL de la requête HTTP en prod, pas sur `site`.\n\n```astro\n---\n// src/layouts/BaseLayout.astro\nconst canonicalURL = new URL(Astro.url.pathname, Astro.site);\n---\n\u003Clink rel=\"canonical\" href={canonicalURL.href} />\n```\n\nMais `Astro.site` retourne la valeur de la propriété `site` dans `astro.config.mjs`. En prod statique sur Vercel, `Astro.site` vaut `https://build.local`. Le canonical rendu est donc :\n\n```html\n\u003Clink rel=\"canonical\" href=\"https://build.local/canapes/canape-angle-velours-bleu\" />\n```\n\nDouble peine. Le sitemap ET les canonicals pointent vers `build.local`. Le navigateur ne montre rien d'anormal — l'utilisateur humain ne regarde pas les balises `\u003Clink>` dans le `\u003Chead>`. Mais Googlebot voit deux signaux cohérents qui disent : \"L'URL officielle de cette page est sur `build.local`.\"\n\nPour vérifier, un `curl` sur le HTML brut suffit :\n\n```bash\ncurl -s https://www.meubles-design.example/canapes/canape-angle-velours-bleu \\\n  | grep -i 'canonical\\|og:url'\n```\n\nRésultat :\n\n```html\n\u003Clink rel=\"canonical\" href=\"https://build.local/canapes/canape-angle-velours-bleu\" />\n\u003Cmeta property=\"og:url\" content=\"https://build.local/canapes/canape-angle-velours-bleu\" />\n```\n\nLes trois signaux — sitemap, canonical, og:url — convergent vers un domaine inexistant.\n\n### Pourquoi les tests n'ont rien détecté\n\nL'équipe avait trois couches de tests :\n\n1. **Tests unitaires** sur les composants Astro — aucun ne teste le rendu HTML des balises `\u003Chead>`.\n2. **Lighthouse CI** en post-build — il vérifie les scores de performance, pas le contenu du sitemap ni la valeur des canonicals.\n3. **Preview Vercel** — l'URL de preview est `https://meubles-design-git-main-team.vercel.app`. Personne n'a ouvert le sitemap de la preview. Et même en l'ouvrant, les URLs `build.local` n'auraient pas semblé plus suspectes que les URLs de preview — aucune ne correspondait à la prod.\n\nLe problème fondamental : aucun test automatisé ne comparait les URLs générées dans le sitemap au domaine de production attendu. C'est un test qui n'existe presque jamais dans les pipelines CI classiques.\n\nUn crawl Screaming Frog du site de production aurait immédiatement signalé le problème : chaque canonical et og:url aurait été flaggé comme \"URL ne correspondant pas au domaine crawlé\". Mais le dernier crawl complet datait d'avant la migration.\n\n### L'ampleur du problème\n\nLe site contient 4 127 pages indexables : 3 840 fiches produit, 187 pages de catégories, 54 pages de contenu éditorial, 46 pages techniques (CGV, FAQ, guides). Le sitemap `sitemap-0.xml` les liste toutes avec le domaine `build.local`.\n\nPendant 11 jours :\n- Google a crawlé le sitemap 3 fois (visible dans le rapport Sitemaps de Search Console).\n- À chaque crawl, il a tenté de résoudre `build.local`. Échec DNS. Les 4 127 URLs ont été ignorées.\n- Le crawl organique via les liens internes continuait, mais Google trouvait des canonicals pointant vers `build.local`, ce qui créait une confusion de signal.\n- Les 23 nouvelles fiches produit ajoutées pendant cette période n'avaient aucun lien interne ancien — elles dépendaient du sitemap pour la découverte. Elles n'ont pas été indexées.\n\nCe type de régression silencieuse rappelle les incidents de [canonicals pointant vers le staging](/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging), à la différence que le domaine ici n'a jamais existé du tout.\n\n## Le fix : variable d'environnement, validation au build, et tests de sitemap\n\n### Étape 1 — Le patch immédiat\n\nLe correctif minimal : ajouter `SITE_URL` dans les secrets GitHub Actions et durcir le fallback pour qu'il échoue bruyamment plutôt que silencieusement.\n\n```typescript\n// astro.config.mjs — version corrigée\nimport { defineConfig } from 'astro/config';\nimport sitemap from '@astrojs/sitemap';\n\nconst siteUrl = process.env.SITE_URL;\n\nif (!siteUrl) {\n  throw new Error(\n    '[astro.config] SITE_URL is not defined. ' +\n    'Set it in your environment variables. ' +\n    'Build aborted to prevent sitemap/canonical corruption.'\n  );\n}\n\ntry {\n  new URL(siteUrl);\n} catch {\n  throw new Error(\n    `[astro.config] SITE_URL \"${siteUrl}\" is not a valid URL. Build aborted.`\n  );\n}\n\nexport default defineConfig({\n  site: siteUrl,\n  integrations: [\n    sitemap({\n      changefreq: 'weekly',\n      priority: 0.7,\n      lastmod: new Date(),\n    }),\n  ],\n  output: 'static',\n  build: {\n    format: 'directory',\n  },\n});\n```\n\nPlus de fallback silencieux. Si `SITE_URL` est absent, le build plante immédiatement avec un message explicite. Le `new URL()` valide en plus que la valeur est une URL syntaxiquement correcte.\n\nLe workflow CI est mis à jour :\n\n```yaml\n# .github/workflows/deploy.yml — section corrigée\n      - run: npm run build\n        env:\n          NODE_ENV: production\n          SITE_URL: ${{ secrets.SITE_URL }}\n          STRAPI_API_URL: ${{ secrets.STRAPI_API_URL }}\n          STRAPI_API_TOKEN: ${{ secrets.STRAPI_API_TOKEN }}\n```\n\nLe secret `SITE_URL` est ajouté dans les settings du repository GitHub avec la valeur `https://www.meubles-design.example`.\n\n### Étape 2 — Rebuild et redéploiement\n\nLe rebuild est lancé à 11h30. Le sitemap généré est vérifié avant que le déploiement ne soit promu en production :\n\n```bash\n# Vérification post-build, avant deploy\ngrep -c 'build.local' dist/sitemap-0.xml\n# Attendu : 0\n\ngrep -c 'www.meubles-design.example' dist/sitemap-0.xml\n# Attendu : 4127\n\nhead -20 dist/sitemap-0.xml\n```\n\nLe sitemap est propre. Le déploiement passe en production à 11h47.\n\n### Étape 3 — Forcer le recrawl\n\nLe lead SEO soumet à nouveau le sitemap dans Search Console. Il utilise aussi l'outil d'inspection d'URL sur les 23 fiches produit non indexées pour demander une indexation individuelle.\n\nLes canonicals dans le HTML sont également corrigés par le rebuild — puisque `Astro.site` retourne désormais la bonne valeur.\n\nVérification post-déploiement :\n\n```bash\ncurl -s https://www.meubles-design.example/canapes/canape-angle-velours-bleu \\\n  | grep -i 'canonical\\|og:url'\n```\n\n```html\n\u003Clink rel=\"canonical\" href=\"https://www.meubles-design.example/canapes/canape-angle-velours-bleu\" />\n\u003Cmeta property=\"og:url\" content=\"https://www.meubles-design.example/canapes/canape-angle-velours-bleu\" />\n```\n\nCorrect.\n\n### Étape 4 — Test automatisé dans le pipeline\n\nL'équipe ajoute un script de validation post-build dans le CI :\n\n```bash\n#!/bin/bash\n# scripts/validate-sitemap.sh\n\nEXPECTED_DOMAIN=\"www.meubles-design.example\"\nSITEMAP=\"dist/sitemap-0.xml\"\n\nif [ ! -f \"$SITEMAP\" ]; then\n  echo \"❌ Sitemap not found at $SITEMAP\"\n  exit 1\nfi\n\nBAD_URLS=$(grep -c '\u003Cloc>' \"$SITEMAP\")\nGOOD_URLS=$(grep \"\u003Cloc>\" \"$SITEMAP\" | grep -c \"$EXPECTED_DOMAIN\")\n\nif [ \"$BAD_URLS\" -ne \"$GOOD_URLS\" ]; then\n  echo \"❌ Sitemap contains URLs not matching $EXPECTED_DOMAIN\"\n  echo \"Total \u003Cloc> entries: $BAD_URLS\"\n  echo \"Matching $EXPECTED_DOMAIN: $GOOD_URLS\"\n  grep \"\u003Cloc>\" \"$SITEMAP\" | grep -v \"$EXPECTED_DOMAIN\" | head -5\n  exit 1\nfi\n\necho \"✅ Sitemap OK: $GOOD_URLS URLs, all on $EXPECTED_DOMAIN\"\n```\n\nCe script est appelé dans le workflow CI entre le build et le déploiement. Si un seul `\u003Cloc>` ne contient pas le bon domaine, le pipeline échoue.\n\n### Temps de récupération\n\n- **J+2** : Google a recrawlé le sitemap. Les URLs sont de nouveau reconnues.\n- **J+5** : les 23 fiches produit manquantes commencent à apparaître dans l'index.\n- **J+9** : le taux de crawl remonte à 310 requêtes/jour (niveau pré-incident).\n- **J+14** : la couverture Search Console revient à 4 100+ pages indexées. Pas de perte de position mesurable sur les mots-clés historiques — le crawl via liens internes avait maintenu l'essentiel.\n\nLe vrai coût : 11 jours d'indexation perdue pour les nouvelles pages, et un risque réputationnel si Google avait commencé à désindexer les pages existantes faute de canonical cohérent. Sur un site à 18 000 sessions organiques/mois, le lead SEO estime le manque à gagner à environ 850 sessions sur la période — essentiellement du trafic longue traîne sur les nouvelles fiches.\n\nCe schéma — un domaine de staging ou de build qui fuit en production — est un classique des migrations de framework. L'incident est structurellement identique aux [redirections oubliées lors de migrations CMS](/blog/migration-wordpress-vers-headless-strapi-4000-redirections-htaccess-oubliees) ou aux [problèmes de configuration RSS orpheline lors de passages à Astro](/blog/migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines) : un artefact de build qui n'est jamais testé parce qu'il est invisible dans le navigateur.\n\n## Ce qu'on en retient\n\nTrois règles émergent de cet incident.\n\nUn : ne jamais utiliser de fallback silencieux pour la propriété `site` d'Astro. Si la variable d'environnement manque, le build doit planter. Un build cassé vaut infiniment mieux qu'un sitemap corrompu déployé en production.\n\nDeux : tester le sitemap dans le CI. Pas visuellement. Pas manuellement. Un `grep` de 10 lignes suffit à vérifier que chaque `\u003Cloc>` contient le bon domaine.\n\nTrois : la divergence entre \"ce que voit le navigateur\" et \"ce que voit le crawler\" est le terrain de jeu favori des régressions SEO. Un outil de monitoring continu comme Seogard détecte ce type de divergence — canonicals incohérents, sitemap pointant vers un mauvais domaine — en quelques minutes après le déploiement, pas onze jours plus tard dans un rapport Search Console.\n\nLe sitemap est un fichier statique. Il est généré une fois, déployé une fois, et personne ne le regarde. C'est exactement pour ça qu'il casse en silence.\n```","https://seogard.io/blog/astro-site-config-build-local-le-sitemap-entier-pointe-vers-un-domaine-inexistant","Framework","2026-06-07T16:01:48.153Z","2026-06-07","Un site Astro envoie un sitemap.xml avec 4 000 URLs build.local à Google. Récit de l'incident, diagnostic technique et fix complet.","\u003Ch1>Sitemap Astro pointant vers build.local : 4 000 URLs envoyées dans le vide\u003C/h1>\n\u003Cp>Mercredi 11h. Le lead SEO d'une marketplace de mobilier design ouvre Search Console pour vérifier la couverture post-déploiement. Le rapport Sitemaps affiche un statut vert — \"Réussite\". Mais la colonne \"URL découvertes\" indique 0 pages indexées sur les 4 127 soumises. Il clique sur le sitemap. Chaque \u003Ccode>&#x3C;loc>\u003C/code> commence par \u003Ccode>https://build.local/\u003C/code>. Le domaine de production n'apparaît nulle part. Depuis 11 jours, Google ingère un plan de site qui pointe vers un domaine qui n'existe pas.\u003C/p>\n\u003Ch2>Lundi 9h12 — Le silence avant l'alerte\u003C/h2>\n\u003Cp>Le déploiement incriminé remonte au vendredi d'avant. L'équipe front a migré le site de Astro 4.x vers Astro 5, en profitant pour restructurer la configuration de build. Le pipeline CI/CD tourne sur GitHub Actions. Le site est hébergé sur Vercel. Tout passe au vert : build réussie, preview fonctionnelle, lighthouse score stable.\u003C/p>\n\u003Cp>Personne ne regarde le sitemap.\u003C/p>\n\u003Cp>Lundi matin, le lead SEO vérifie les positions sur un panel de 85 mots-clés prioritaires. Pas de mouvement anormal — Google n'a pas encore recrawlé massivement. Mais il note un détail : le dernier crawl du sitemap dans Search Console date de vendredi 17h23. Statut \"Réussite\", mais 0 URLs indexées depuis ce crawl.\u003C/p>\n\u003Cp>Il télécharge le fichier \u003Ccode>sitemap-0.xml\u003C/code> directement depuis \u003Ccode>https://www.meubles-design.example/sitemap-0.xml\u003C/code>. Le fichier se charge. Il fait 312 Ko. Il l'ouvre.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;?\u003C/span>\u003Cspan style=\"color:#85E89D\">xml\u003C/span>\u003Cspan style=\"color:#B392F0\"> version\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"1.0\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> encoding\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"UTF-8\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">?>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">urlset\u003C/span>\u003Cspan style=\"color:#B392F0\"> xmlns\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"http://www.sitemaps.org/schemas/sitemap/0.9\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>https://build.local/canapes/canape-angle-velours-bleu&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>2026-05-29T14:22:00.000Z&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">changefreq\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>weekly&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">changefreq\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">priority\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>0.7&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">priority\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>https://build.local/tables/table-basse-chene-massif&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">loc\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>2026-05-29T14:22:00.000Z&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">lastmod\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">changefreq\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>weekly&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">changefreq\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    &#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">priority\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>0.7&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">priority\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  &#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">url\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">  &#x3C;!-- 4 125 autres entrées identiques -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;/\u003C/span>\u003Cspan style=\"color:#85E89D\">urlset\u003C/span>\u003Cspan style=\"color:#E1E4E8\">>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Chaque URL pointe vers \u003Ccode>https://build.local\u003C/code>. Pas \u003Ccode>https://www.meubles-design.example\u003C/code>.\u003C/p>\n\u003Cp>Le lead SEO envoie un message Slack à 9h34 : \"Le sitemap est cassé. Toutes les URLs pointent vers un domaine local. Depuis vendredi.\"\u003C/p>\n\u003Cp>L'équipe front vérifie d'abord le plugin \u003Ccode>@astrojs/sitemap\u003C/code>. Il est bien installé, bien importé. Le build local génère le même fichier. Le dev principal pense d'abord à un bug du plugin — il ouvre une issue GitHub, cherche des rapports similaires. Rien de concluant.\u003C/p>\n\u003Cp>À 10h15, le CTO demande l'impact. Le lead SEO sort les chiffres de Search Console sur les 10 derniers jours : le taux de crawl des URLs de contenu a chuté de 340 requêtes/jour à 87. Google continue de crawler le site via les liens internes, mais il ignore le sitemap — logiquement, puisque les URLs \u003Ccode>build.local\u003C/code> ne résolvent vers aucune IP. Le trafic organique n'a pas encore décroché visiblement, mais les nouvelles pages produit publiées depuis vendredi — 23 fiches — n'ont été indexées nulle part.\u003C/p>\n\u003Cp>À 10h42, le dev junior lâche dans le thread Slack : \"J'ai trouvé. C'est le \u003Ccode>astro.config.mjs\u003C/code>.\"\u003C/p>\n\u003Ch2>Le bug : \u003Ccode>site\u003C/code> configuré par environnement, mais l'environnement n'existe pas en CI\u003C/h2>\n\u003Cp>Le problème est dans la propriété \u003Ccode>site\u003C/code> du fichier \u003Ccode>astro.config.mjs\u003C/code>. Cette propriété est \u003Ca href=\"https://docs.astro.build/en/reference/configuration-reference/#site\">documentée par Astro\u003C/a> comme la source de vérité pour générer les URLs absolues — dans le sitemap, dans les flux RSS, dans les balises canonical.\u003C/p>\n\u003Cp>Voici la configuration telle qu'elle existait avant la migration Astro 5 :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// astro.config.mjs — version AVANT migration (Astro 4.x)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { defineConfig } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> sitemap \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@astrojs/sitemap'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#B392F0\"> defineConfig\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  site: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https://www.meubles-design.example'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  integrations: [\u003C/span>\u003Cspan style=\"color:#B392F0\">sitemap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">()],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">});\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Direct. Fonctionnel. Le domaine de production est en dur.\u003C/p>\n\u003Cp>Lors de la migration vers Astro 5, l'équipe front a voulu \"professionnaliser\" la config. Le dev principal a introduit une gestion par variable d'environnement, pour distinguer les builds de développement, de staging et de production :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// astro.config.mjs — version APRÈS migration (Astro 5)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { defineConfig } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> sitemap \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@astrojs/sitemap'\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\"> siteUrl\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">SITE_URL\u003C/span>\u003Cspan style=\"color:#F97583\"> ||\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'https://build.local'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> default\u003C/span>\u003Cspan style=\"color:#B392F0\"> defineConfig\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  site: siteUrl,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  integrations: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    sitemap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      changefreq: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'weekly'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      priority: \u003C/span>\u003Cspan style=\"color:#79B8FF\">0.7\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      lastmod: \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\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\">  output: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'static'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  build: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    format: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory'\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>\u003C/code>\u003C/pre>\n\u003Cp>La logique : en local, \u003Ccode>SITE_URL\u003C/code> n'est pas défini, donc le fallback \u003Ccode>https://build.local\u003C/code> s'applique. En CI, la variable devrait être injectée.\u003C/p>\n\u003Cp>Le problème : elle ne l'est pas.\u003C/p>\n\u003Cp>Le workflow GitHub Actions ressemble à ceci :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># .github/workflows/deploy.yml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">name\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">Deploy to Vercel\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  push\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    branches\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: [\u003C/span>\u003Cspan style=\"color:#9ECBFF\">main\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">jobs\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">  build-and-deploy\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    runs-on\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">ubuntu-latest\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">    steps\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">actions/checkout@v4\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">actions/setup-node@v4\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        with\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          node-version\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">20\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">npm ci\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">npm run build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        env\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          NODE_ENV\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">production\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          STRAPI_API_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.STRAPI_API_URL }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          STRAPI_API_TOKEN\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.STRAPI_API_TOKEN }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">          # SITE_URL manquant ici\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">uses\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">amondnet/vercel-action@v25\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        with\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          vercel-token\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.VERCEL_TOKEN }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          vercel-org-id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.VERCEL_ORG_ID }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          vercel-project-id\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.VERCEL_PROJECT_ID }}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>\u003Ccode>SITE_URL\u003C/code> n'est jamais défini dans les secrets GitHub, ni dans les variables d'environnement du job de build. Le fallback \u003Ccode>'https://build.local'\u003C/code> s'applique silencieusement. Astro ne lève aucun warning. Le build réussit. Le sitemap est généré. Vercel déploie le résultat.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Le développeur ouvre le site sur \u003Ccode>https://www.meubles-design.example/canapes/canape-angle-velours-bleu\u003C/code>. La page se charge. Le HTML est correct. Les balises meta sont en place. Le canonical pointe vers la bonne URL — parce que l'équipe utilise \u003Ccode>Astro.url\u003C/code> dans le layout, qui se base sur l'URL de la requête HTTP en prod, pas sur \u003Ccode>site\u003C/code>.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/layouts/BaseLayout.astro\u003C/span>\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:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(Astro.url.pathname, Astro.site);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">---\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">={canonicalURL.href} />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Mais \u003Ccode>Astro.site\u003C/code> retourne la valeur de la propriété \u003Ccode>site\u003C/code> dans \u003Ccode>astro.config.mjs\u003C/code>. En prod statique sur Vercel, \u003Ccode>Astro.site\u003C/code> vaut \u003Ccode>https://build.local\u003C/code>. Le canonical rendu est donc :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://build.local/canapes/canape-angle-velours-bleu\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Double peine. Le sitemap ET les canonicals pointent vers \u003Ccode>build.local\u003C/code>. Le navigateur ne montre rien d'anormal — l'utilisateur humain ne regarde pas les balises \u003Ccode>&#x3C;link>\u003C/code> dans le \u003Ccode>&#x3C;head>\u003C/code>. Mais Googlebot voit deux signaux cohérents qui disent : \"L'URL officielle de cette page est sur \u003Ccode>build.local\u003C/code>.\"\u003C/p>\n\u003Cp>Pour vérifier, un \u003Ccode>curl\u003C/code> sur le HTML brut suffit :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.meubles-design.example/canapes/canape-angle-velours-bleu\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'canonical\\|og:url'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://build.local/canapes/canape-angle-velours-bleu\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:url\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://build.local/canapes/canape-angle-velours-bleu\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Les trois signaux — sitemap, canonical, og:url — convergent vers un domaine inexistant.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>L'équipe avait trois couches de tests :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Tests unitaires\u003C/strong> sur les composants Astro — aucun ne teste le rendu HTML des balises \u003Ccode>&#x3C;head>\u003C/code>.\u003C/li>\n\u003Cli>\u003Cstrong>Lighthouse CI\u003C/strong> en post-build — il vérifie les scores de performance, pas le contenu du sitemap ni la valeur des canonicals.\u003C/li>\n\u003Cli>\u003Cstrong>Preview Vercel\u003C/strong> — l'URL de preview est \u003Ccode>https://meubles-design-git-main-team.vercel.app\u003C/code>. Personne n'a ouvert le sitemap de la preview. Et même en l'ouvrant, les URLs \u003Ccode>build.local\u003C/code> n'auraient pas semblé plus suspectes que les URLs de preview — aucune ne correspondait à la prod.\u003C/li>\n\u003C/ol>\n\u003Cp>Le problème fondamental : aucun test automatisé ne comparait les URLs générées dans le sitemap au domaine de production attendu. C'est un test qui n'existe presque jamais dans les pipelines CI classiques.\u003C/p>\n\u003Cp>Un crawl Screaming Frog du site de production aurait immédiatement signalé le problème : chaque canonical et og:url aurait été flaggé comme \"URL ne correspondant pas au domaine crawlé\". Mais le dernier crawl complet datait d'avant la migration.\u003C/p>\n\u003Ch3>L'ampleur du problème\u003C/h3>\n\u003Cp>Le site contient 4 127 pages indexables : 3 840 fiches produit, 187 pages de catégories, 54 pages de contenu éditorial, 46 pages techniques (CGV, FAQ, guides). Le sitemap \u003Ccode>sitemap-0.xml\u003C/code> les liste toutes avec le domaine \u003Ccode>build.local\u003C/code>.\u003C/p>\n\u003Cp>Pendant 11 jours :\u003C/p>\n\u003Cul>\n\u003Cli>Google a crawlé le sitemap 3 fois (visible dans le rapport Sitemaps de Search Console).\u003C/li>\n\u003Cli>À chaque crawl, il a tenté de résoudre \u003Ccode>build.local\u003C/code>. Échec DNS. Les 4 127 URLs ont été ignorées.\u003C/li>\n\u003Cli>Le crawl organique via les liens internes continuait, mais Google trouvait des canonicals pointant vers \u003Ccode>build.local\u003C/code>, ce qui créait une confusion de signal.\u003C/li>\n\u003Cli>Les 23 nouvelles fiches produit ajoutées pendant cette période n'avaient aucun lien interne ancien — elles dépendaient du sitemap pour la découverte. Elles n'ont pas été indexées.\u003C/li>\n\u003C/ul>\n\u003Cp>Ce type de régression silencieuse rappelle les incidents de \u003Ca href=\"/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging\">canonicals pointant vers le staging\u003C/a>, à la différence que le domaine ici n'a jamais existé du tout.\u003C/p>\n\u003Ch2>Le fix : variable d'environnement, validation au build, et tests de sitemap\u003C/h2>\n\u003Ch3>Étape 1 — Le patch immédiat\u003C/h3>\n\u003Cp>Le correctif minimal : ajouter \u003Ccode>SITE_URL\u003C/code> dans les secrets GitHub Actions et durcir le fallback pour qu'il échoue bruyamment plutôt que silencieusement.\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// astro.config.mjs — version corrigée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { defineConfig } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro/config'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> sitemap \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@astrojs/sitemap'\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\"> siteUrl\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">SITE_URL\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\">siteUrl) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  throw\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Error\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    '[astro.config] SITE_URL is not defined. '\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Set it in your environment variables. '\u003C/span>\u003Cspan style=\"color:#F97583\"> +\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    'Build aborted to prevent sitemap/canonical corruption.'\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\">try\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  new\u003C/span>\u003Cspan style=\"color:#B392F0\"> URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(siteUrl);\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\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  throw\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Error\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    `[astro.config] SITE_URL \"${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">siteUrl\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\" is not a valid URL. Build aborted.`\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:#B392F0\"> defineConfig\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  site: siteUrl,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  integrations: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">    sitemap\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      changefreq: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'weekly'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      priority: \u003C/span>\u003Cspan style=\"color:#79B8FF\">0.7\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      lastmod: \u003C/span>\u003Cspan style=\"color:#F97583\">new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Date\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\">  output: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'static'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  build: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    format: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'directory'\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>\u003C/code>\u003C/pre>\n\u003Cp>Plus de fallback silencieux. Si \u003Ccode>SITE_URL\u003C/code> est absent, le build plante immédiatement avec un message explicite. Le \u003Ccode>new URL()\u003C/code> valide en plus que la valeur est une URL syntaxiquement correcte.\u003C/p>\n\u003Cp>Le workflow CI est mis à jour :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># .github/workflows/deploy.yml — section corrigée\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      - \u003C/span>\u003Cspan style=\"color:#85E89D\">run\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">npm run build\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">        env\u003C/span>\u003Cspan style=\"color:#E1E4E8\">:\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          NODE_ENV\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">production\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          SITE_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.SITE_URL }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          STRAPI_API_URL\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.STRAPI_API_URL }}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#85E89D\">          STRAPI_API_TOKEN\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">${{ secrets.STRAPI_API_TOKEN }}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le secret \u003Ccode>SITE_URL\u003C/code> est ajouté dans les settings du repository GitHub avec la valeur \u003Ccode>https://www.meubles-design.example\u003C/code>.\u003C/p>\n\u003Ch3>Étape 2 — Rebuild et redéploiement\u003C/h3>\n\u003Cp>Le rebuild est lancé à 11h30. Le sitemap généré est vérifié avant que le déploiement ne soit promu en production :\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érification post-build, avant deploy\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'build.local'\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> dist/sitemap-0.xml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Attendu : 0\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'www.meubles-design.example'\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> dist/sitemap-0.xml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Attendu : 4127\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">head\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -20\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> dist/sitemap-0.xml\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le sitemap est propre. Le déploiement passe en production à 11h47.\u003C/p>\n\u003Ch3>Étape 3 — Forcer le recrawl\u003C/h3>\n\u003Cp>Le lead SEO soumet à nouveau le sitemap dans Search Console. Il utilise aussi l'outil d'inspection d'URL sur les 23 fiches produit non indexées pour demander une indexation individuelle.\u003C/p>\n\u003Cp>Les canonicals dans le HTML sont également corrigés par le rebuild — puisque \u003Ccode>Astro.site\u003C/code> retourne désormais la bonne valeur.\u003C/p>\n\u003Cp>Vérification post-déploiement :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://www.meubles-design.example/canapes/canape-angle-velours-bleu\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -i\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'canonical\\|og:url'\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003Cspan style=\"color:#B392F0\"> rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"canonical\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.meubles-design.example/canapes/canape-angle-velours-bleu\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">meta\u003C/span>\u003Cspan style=\"color:#B392F0\"> property\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"og:url\"\u003C/span>\u003Cspan style=\"color:#B392F0\"> content\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://www.meubles-design.example/canapes/canape-angle-velours-bleu\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> />\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Correct.\u003C/p>\n\u003Ch3>Étape 4 — Test automatisé dans le pipeline\u003C/h3>\n\u003Cp>L'équipe ajoute un script de validation post-build dans le CI :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">#!/bin/bash\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># scripts/validate-sitemap.sh\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">EXPECTED_DOMAIN\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"www.meubles-design.example\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">SITEMAP\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"dist/sitemap-0.xml\"\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:#F97583\"> -f\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SITEMAP\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"❌ Sitemap not found at \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SITEMAP\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  exit\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">BAD_URLS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#B392F0\">grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '&#x3C;loc>'\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SITEMAP\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">GOOD_URLS\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$(\u003C/span>\u003Cspan style=\"color:#B392F0\">grep\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"&#x3C;loc>\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SITEMAP\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -c\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$EXPECTED_DOMAIN\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:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$BAD_URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> -ne\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$GOOD_URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ]; \u003C/span>\u003Cspan style=\"color:#F97583\">then\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"❌ Sitemap contains URLs not matching \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$EXPECTED_DOMAIN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Total &#x3C;loc> entries: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$BAD_URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"Matching \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$EXPECTED_DOMAIN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$GOOD_URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  grep\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"&#x3C;loc>\"\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$SITEMAP\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> grep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -v\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">$EXPECTED_DOMAIN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> head\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -5\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  exit\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 1\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">fi\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">echo\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"✅ Sitemap OK: \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$GOOD_URLS\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> URLs, all on \u003C/span>\u003Cspan style=\"color:#E1E4E8\">$EXPECTED_DOMAIN\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Ce script est appelé dans le workflow CI entre le build et le déploiement. Si un seul \u003Ccode>&#x3C;loc>\u003C/code> ne contient pas le bon domaine, le pipeline échoue.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+2\u003C/strong> : Google a recrawlé le sitemap. Les URLs sont de nouveau reconnues.\u003C/li>\n\u003Cli>\u003Cstrong>J+5\u003C/strong> : les 23 fiches produit manquantes commencent à apparaître dans l'index.\u003C/li>\n\u003Cli>\u003Cstrong>J+9\u003C/strong> : le taux de crawl remonte à 310 requêtes/jour (niveau pré-incident).\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : la couverture Search Console revient à 4 100+ pages indexées. Pas de perte de position mesurable sur les mots-clés historiques — le crawl via liens internes avait maintenu l'essentiel.\u003C/li>\n\u003C/ul>\n\u003Cp>Le vrai coût : 11 jours d'indexation perdue pour les nouvelles pages, et un risque réputationnel si Google avait commencé à désindexer les pages existantes faute de canonical cohérent. Sur un site à 18 000 sessions organiques/mois, le lead SEO estime le manque à gagner à environ 850 sessions sur la période — essentiellement du trafic longue traîne sur les nouvelles fiches.\u003C/p>\n\u003Cp>Ce schéma — un domaine de staging ou de build qui fuit en production — est un classique des migrations de framework. L'incident est structurellement identique aux \u003Ca href=\"/blog/migration-wordpress-vers-headless-strapi-4000-redirections-htaccess-oubliees\">redirections oubliées lors de migrations CMS\u003C/a> ou aux \u003Ca href=\"/blog/migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines\">problèmes de configuration RSS orpheline lors de passages à Astro\u003C/a> : un artefact de build qui n'est jamais testé parce qu'il est invisible dans le navigateur.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Trois règles émergent de cet incident.\u003C/p>\n\u003Cp>Un : ne jamais utiliser de fallback silencieux pour la propriété \u003Ccode>site\u003C/code> d'Astro. Si la variable d'environnement manque, le build doit planter. Un build cassé vaut infiniment mieux qu'un sitemap corrompu déployé en production.\u003C/p>\n\u003Cp>Deux : tester le sitemap dans le CI. Pas visuellement. Pas manuellement. Un \u003Ccode>grep\u003C/code> de 10 lignes suffit à vérifier que chaque \u003Ccode>&#x3C;loc>\u003C/code> contient le bon domaine.\u003C/p>\n\u003Cp>Trois : la divergence entre \"ce que voit le navigateur\" et \"ce que voit le crawler\" est le terrain de jeu favori des régressions SEO. Un outil de monitoring continu comme Seogard détecte ce type de divergence — canonicals incohérents, sitemap pointant vers un mauvais domaine — en quelques minutes après le déploiement, pas onze jours plus tard dans un rapport Search Console.\u003C/p>\n\u003Cp>Le sitemap est un fichier statique. Il est généré une fois, déployé une fois, et personne ne le regarde. C'est exactement pour ça qu'il casse en silence.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"astro","sitemap","site config","build","Astro sitemap pointe vers build.local : 4 000 URLs perdues","Sun Jun 07 2026 16:01:48 GMT+0000 (Coordinated Universal Time)",[25,40,55,70,85,98],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":29,"createdAt":30,"date":12,"description":31,"image":15,"imageAlt":15,"readingTime":32,"tags":33,"title":38,"updatedAt":39},"6a250954aa6b273b0c8358fe","refonte-typo-variable-font-lazy-load-qui-degrade-les-core-web-vitals","https://seogard.io/blog/refonte-typo-variable-font-lazy-load-qui-degrade-les-core-web-vitals","Performance","2026-06-07T06:01:56.599Z","Une refonte typo charge la police en lazy. Le LCP passe de 1.8s à 3.0s. Aucune meta ne bouge. Le trafic chute de 18%. Récit, diagnostic, fix.",12,[34,35,36,37],"core web vitals","lcp","font","performance","Variable font lazy-load : LCP dégradé de 1.2s, ranking en chute","Sun Jun 07 2026 06:01:56 GMT+0000 (Coordinated Universal Time)",{"_id":41,"slug":42,"__v":6,"author":7,"canonical":43,"category":44,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":32,"tags":48,"title":53,"updatedAt":54},"6a23b7d0aa6b273b0c6c840f","splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","https://seogard.io/blog/splash-screen-noscript-mal-place-qui-contient-le-vrai-contenu-pour-googlebot-sansjs","Rendering","2026-06-06T06:01:52.615Z","2026-06-06","Un e-commerce SPA cache son contenu dans une balise noscript pour les bots. Google détecte du cloaking. Récit, diagnostic et fix complet.",[49,50,51,52],"noscript","cloaking","spa","splash","noscript cloaking : splash screen SPA piège Google","Sat Jun 06 2026 06:01:52 GMT+0000 (Coordinated Universal Time)",{"_id":56,"slug":57,"__v":6,"author":7,"canonical":58,"category":59,"createdAt":60,"date":46,"description":61,"image":15,"imageAlt":15,"readingTime":32,"tags":62,"title":68,"updatedAt":69},"6a2444b8aa6b273b0ce0dbc7","cloudflare-bots-now-make-up-57-of-webpage-requests","https://seogard.io/blog/cloudflare-bots-now-make-up-57-of-webpage-requests","Actualités SEO","2026-06-06T16:03:04.236Z","Cloudflare révèle que 57% des requêtes web sont des bots. Analyse technique des impacts SEO et stratégies concrètes pour protéger votre crawl budget.",[63,64,65,66,67],"cloudflare","bots","crawl budget","seo technique","trafic automatisé","57% de bots : impact SEO et stratégies de défense technique","Sat Jun 06 2026 16:03:04 GMT+0000 (Coordinated Universal Time)",{"_id":71,"slug":72,"__v":6,"author":7,"canonical":73,"category":59,"createdAt":74,"date":75,"description":76,"image":15,"imageAlt":15,"readingTime":32,"tags":77,"title":83,"updatedAt":84},"6a226691aa6b273b0c562a67","google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","https://seogard.io/blog/google-s-may-core-update-favored-pages-that-match-intent-via-sejournal-mattgsouthern","2026-06-05T06:02:57.864Z","2026-06-05","Analyse technique du May 2025 Core Update de Google : comment l'alignement intent/contenu et les signaux techniques déterminent les gagnants et perdants.",[78,79,80,81,82],"core update","search intent","SISTRIX","SEO technique","google algorithm","May 2025 Core Update : intent matching et signaux techniques","Fri Jun 05 2026 06:02:57 GMT+0000 (Coordinated Universal Time)",{"_id":86,"slug":87,"__v":6,"author":7,"canonical":88,"category":89,"createdAt":90,"date":75,"description":91,"image":15,"imageAlt":15,"readingTime":32,"tags":92,"title":96,"updatedAt":97},"6a22f2ebaa6b273b0cc9fc68","mode-sombre-prefers-color-scheme-injecte-un-meta-noindex-par-erreur","https://seogard.io/blog/mode-sombre-prefers-color-scheme-injecte-un-meta-noindex-par-erreur","Refonte","2026-06-05T16:01:47.389Z","Un CSS-in-JS mal configuré injecte un meta noindex via prefers-color-scheme. Récit de l'incident, diagnostic technique, et correctif complet.",[93,94,95],"dark mode","css in js","noindex","Dark mode CSS-in-JS injecte un noindex : récit et fix","Fri Jun 05 2026 16:01:47 GMT+0000 (Coordinated Universal Time)",{"_id":99,"slug":100,"__v":6,"author":7,"canonical":101,"category":89,"createdAt":102,"date":103,"description":104,"image":15,"imageAlt":15,"readingTime":16,"tags":105,"title":110,"updatedAt":111},"6a2114ceaa6b273b0c3f5edb","design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree","https://seogard.io/blog/design-system-composant-heading-qui-rend-div-selon-la-prop-as-mal-configuree","2026-06-04T06:01:50.617Z","2026-06-04","Un composant Heading React mal configuré rend des div au lieu de h1-h6. Récit de l'incident, diagnostic du diff, fix et récupération SEO.",[106,107,108,109],"design system","heading","semantic","react","Design system React : un Heading en div détruit la sémantique de 1 200 pages","Thu Jun 04 2026 06:01:50 GMT+0000 (Coordinated Universal Time)",{"categories":113},[114,117,121,124,126,130,133,136,140,144,147,150,153,156,159,162,165,169],{"category":59,"slug":115,"count":116},"actualites-seo",162,{"category":118,"slug":119,"count":120},"Migration","migration",18,{"category":44,"slug":122,"count":123},"rendering",9,{"category":29,"slug":37,"count":125},8,{"category":127,"slug":128,"count":129},"SEO Technique","seo-technique",7,{"category":131,"slug":132,"count":129},"Crawl","crawl",{"category":134,"slug":135,"count":129},"Meta Tags","meta-tags",{"category":137,"slug":138,"count":139},"Architecture","architecture",6,{"category":141,"slug":142,"count":143},"JavaScript SEO","javascript-seo",5,{"category":145,"slug":146,"count":143},"Monitoring","monitoring",{"category":148,"slug":149,"count":143},"Structured Data","structured-data",{"category":89,"slug":151,"count":152},"refonte",4,{"category":154,"slug":155,"count":152},"Redirections","redirections",{"category":157,"slug":158,"count":152},"Outils","outils",{"category":160,"slug":161,"count":152},"E-commerce","e-commerce",{"category":163,"slug":164,"count":152},"Avancé","avance",{"category":166,"slug":167,"count":168},"IA & SEO","ia-seo",3,{"category":170,"slug":171,"count":168},"Contenu","contenu"]