[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fUtxQ_jWRXuVuAp5uW7DXC4WUO2sFgfVeBMDMkzEKMik":3,"$fYNsoi5Tl363bCtzwn6FtsDnCbz2OAnl3gQtMbdVDq2I":24},{"_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},"6a153757aa6b273b0c727dc2","migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines",0,"Equipe Seogard","# Migration Gatsby vers Astro : un RSS feed orphelin pendant 6 semaines\n\nMardi 14 janvier, 22h30. L'équipe contenu d'un éditeur SaaS français (blog technique, 380 articles, 28 000 visites organiques/mois) célèbre le déploiement du nouveau blog. Gatsby 4 est mort, Astro 4.x tourne en production. Le Lighthouse affiche 98. Le design est propre. Tout le monde rentre. Personne ne pense au fichier `/rss.xml`. Six semaines plus tard, un développeur advocate remarque que Feedly affiche \"Feed not found\" sur le blog. L'investigation commence.\n\n## Mercredi 26 février, 9h12 — \"Plus personne ne nous lit ?\"\n\nL'alerte ne vient pas de Search Console. Pas de Slack bot. Pas de monitoring. Elle vient d'un tweet.\n\n> \"Hey @[marque], votre RSS est cassé depuis un moment, Feedly me renvoie une erreur.\"\n\nLe dev advocate forward le message au lead contenu. Première réaction : \"On a un RSS ?\" Oui. Gatsby le générait via `gatsby-plugin-feed`. L'URL était `/rss.xml`. Chaque article publié apparaissait dans le feed, et une poignée de newsletters tierces, trois agrégateurs sectoriels et environ 1 200 abonnés Feedly tiraient ce flux automatiquement.\n\nLe lead contenu ouvre `https://blog.exemple.com/rss.xml` dans un navigateur. Réponse : 404.\n\nIl essaie `/feed.xml`. Réponse : un XML valide. Astro sert le feed depuis `@astrojs/rss`, et le fichier de sortie par défaut est `feed.xml`, pas `rss.xml`.\n\nPremier réflexe : vérifier depuis combien de temps le problème existe. Le déploiement Astro date du 14 janvier. On est le 26 février. 43 jours.\n\nLe lead contenu tire les chiffres. Dans GA4, le segment \"referral\" filtré sur les domaines connus d'agrégateurs (feedly.com, inoreader.com, newsblur.com, theoldreader.com) montre un effondrement net :\n\n- **Décembre** (dernier mois complet sous Gatsby) : 3 840 sessions referral via agrégateurs RSS.\n- **Janvier** (bascule le 14) : 2 100 sessions. La moitié du mois fonctionnait encore.\n- **Février** (au 26) : 310 sessions.\n\nL'inbound RSS représentait 14 % du trafic referral total du blog. Ce canal est tombé à quasi-zéro sans que personne ne le remarque. Aucune alerte. Aucun dashboard ne suivait ce flux. L'équipe SEO surveillait les positions organiques et les Core Web Vitals. Le RSS n'existait dans aucun runbook de migration.\n\nLe CTO demande un état des lieux complet. Combien de sources externes pointent encore vers `/rss.xml` ? Un crawl rapide via Screaming Frog sur le backlink profile (export Ahrefs) révèle 17 liens entrants pointant explicitement vers `/rss.xml`. Trois newsletters sectorielles. Deux portails de curation. Le reste : des pages \"blogroll\" de développeurs individuels.\n\nTous ces liens renvoient désormais un 404. Les agrégateurs ont progressivement abandonné le polling. Feedly, après quelques tentatives en erreur, marque le feed comme inactif. Les 1 200 abonnés ne reçoivent plus rien. Ils ne le savent pas. Ils n'ont reçu aucune notification. Le feed a simplement cessé de se mettre à jour dans leur interface.\n\nPersonne n'a crié. C'est le problème des régressions RSS : les utilisateurs ne reviennent pas se plaindre. Ils oublient.\n\n## Le bug : deux URLs, zéro redirect, un fichier fantôme\n\nPour comprendre comment c'est arrivé, il faut remonter à la configuration des deux stacks.\n\n### Côté Gatsby : le feed historique\n\nDans `gatsby-config.js`, le plugin RSS était configuré ainsi :\n\n```javascript\n// gatsby-config.js (Gatsby 4)\nmodule.exports = {\n  plugins: [\n    {\n      resolve: `gatsby-plugin-feed`,\n      options: {\n        query: `\n          {\n            site {\n              siteMetadata {\n                title\n                description\n                siteUrl\n              }\n            }\n          }\n        `,\n        feeds: [\n          {\n            serialize: ({ query: { site, allMarkdownRemark } }) => {\n              return allMarkdownRemark.nodes.map(node => ({\n                title: node.frontmatter.title,\n                description: node.excerpt,\n                date: node.frontmatter.date,\n                url: site.siteMetadata.siteUrl + node.fields.slug,\n                guid: site.siteMetadata.siteUrl + node.fields.slug,\n              }))\n            },\n            query: `\n              {\n                allMarkdownRemark(sort: {frontmatter: {date: DESC}}) {\n                  nodes {\n                    excerpt\n                    fields { slug }\n                    frontmatter { title date }\n                  }\n                }\n              }\n            `,\n            output: \"/rss.xml\",\n            title: \"Blog Exemple — RSS Feed\",\n          },\n        ],\n      },\n    },\n  ],\n}\n```\n\nLe point clé : `output: \"/rss.xml\"`. C'est cette URL que les agrégateurs connaissent. C'est cette URL qui est linkée dans les `\u003Clink rel=\"alternate\">` du `\u003Chead>` HTML. C'est cette URL que 1 200 personnes ont collée dans Feedly.\n\n### Côté Astro : le nouveau feed\n\nL'équipe installe `@astrojs/rss` et crée le endpoint selon la documentation officielle :\n\n```typescript\n// src/pages/feed.xml.ts (Astro 4.x)\nimport rss from '@astrojs/rss';\nimport { getCollection } from 'astro:content';\nimport type { APIContext } from 'astro';\n\nexport async function GET(context: APIContext) {\n  const posts = await getCollection('blog');\n  return rss({\n    title: 'Blog Exemple',\n    description: 'Articles techniques sur le développement web',\n    site: context.site!,\n    items: posts.map((post) => ({\n      title: post.data.title,\n      pubDate: post.data.date,\n      description: post.data.description,\n      link: `/blog/${post.slug}/`,\n    })),\n    customData: `\u003Clanguage>fr-fr\u003C/language>`,\n  });\n}\n```\n\nLe fichier s'appelle `feed.xml.ts`. Astro génère donc la route `/feed.xml`. Pas `/rss.xml`.\n\nLa documentation d'Astro utilise `feed.xml` dans ses exemples. Le développeur qui a configuré le RSS a suivi la doc. Il n'a pas pensé à vérifier l'URL de l'ancien feed. L'ancien feed n'était documenté nulle part dans le runbook de migration.\n\n### Ce que voit le navigateur vs ce que voient les agrégateurs\n\nUn humain qui visite le blog ne voit rien de cassé. Le `\u003Chead>` du nouveau site contient bien un lien vers le feed — mais vers le *nouveau* :\n\n```html\n\u003C!-- \u003Chead> du site Astro en production -->\n\u003Clink\n  rel=\"alternate\"\n  type=\"application/rss+xml\"\n  title=\"Blog Exemple\"\n  href=\"https://blog.exemple.com/feed.xml\"\n/>\n```\n\nAucun problème pour un nouveau visiteur qui découvre le blog et cherche le RSS. Mais les 1 200 abonnés existants ne revisitent pas la page d'accueil pour trouver le nouveau lien. Leur agrégateur continue de poller `/rss.xml`, qui renvoie un 404.\n\nLes newsletters tierces qui scrappent `/rss.xml` automatiquement se retrouvent face au même 404. Après 3 à 7 jours de 404 consécutifs (selon la politique de retry de chaque agrégateur), la plupart arrêtent le polling. Feedly affiche un badge \"inactive\" après environ 48h de 404. Certains outils comme Inoreader persistent un peu plus longtemps, mais finissent par décrocher aussi.\n\n### Pourquoi les tests n'ont rien détecté\n\nLe checklist de migration contenait 34 points. Voici ce qui était vérifié :\n\n- Redirections 301 de toutes les anciennes URLs d'articles : ✅\n- Sitemap XML présent et soumis à Search Console : ✅\n- Canonical tags corrects : ✅\n- Meta robots : ✅\n- Core Web Vitals post-déploiement : ✅\n- RSS feed : absent de la checklist.\n\nLe crawl Screaming Frog pré-déploiement était configuré pour scanner les URLs du sitemap + les liens internes. Le RSS feed n'apparaissait dans aucun sitemap (c'est normal, les fichiers RSS ne sont pas indexés). Il n'était linké en `\u003Ca href>` nulle part dans le body des pages — uniquement en `\u003Clink rel=\"alternate\">` dans le `\u003Chead>`.\n\nScreaming Frog, par défaut, crawle les `\u003Clink>` du `\u003Chead>`. Mais le crawl de validation a été lancé *après* le déploiement Astro. Le nouveau `\u003Chead>` pointe vers `/feed.xml`, qui répond 200. Le crawl est vert. L'ancien `/rss.xml` n'est référencé nulle part sur le nouveau site. Il n'apparaît donc jamais dans le crawl.\n\nLe problème fondamental : **la régression se situe dans la rupture d'une URL externe qui n'existe plus dans le périmètre du nouveau site.** Les outils de crawl vérifient ce qui existe. Pas ce qui a disparu.\n\nPour détecter cette régression, il aurait fallu un diff entre les URLs servies par l'ancien site et celles servies par le nouveau. Un crawl comparatif. Ou un monitoring des URLs critiques non-HTML (RSS, sitemap, robots.txt, fichiers JSON-LD standalone) avec alerting sur changement de status code.\n\n### L'effet cascade sur le SEO indirect\n\nLe RSS feed ne contribue pas directement au ranking Google. Googlebot ne crawle pas les feeds RSS pour indexer les pages (il utilise le sitemap et les liens internes). Mais le RSS avait un effet indirect mesurable :\n\n1. **Trois newsletters sectorielles** tiraient leur contenu du feed. Chaque article publié générait un lien entrant depuis l'archive de la newsletter. Depuis le 14 janvier, zéro nouveau lien entrant via ce canal.\n2. **Deux portails de curation technique** affichaient les derniers articles du blog via le RSS. Les articles ont disparu de ces portails. Perte de visibilité indirecte.\n3. **Les abonnés Feedly** partageaient régulièrement les articles sur Twitter/LinkedIn. Ce relais social s'est tari.\n\nLe trafic referral RSS (3 800 sessions/mois) n'est pas du trafic organique. Mais la perte de liens entrants générés par les newsletters a un impact organique différé. L'équipe observe, sur les 6 semaines, une baisse de 8 positions moyennes sur 12 mots-clés informationnels à forte concurrence. Corrélation, pas causalité prouvée — mais le timing coïncide.\n\n## Le fix : redirect, alias, et récupération\n\n### Étape 1 : la redirect 301 immédiate\n\nLe fix le plus urgent : rediriger `/rss.xml` vers `/feed.xml`. Sur Astro déployé via Vercel (ce qui est le cas ici), la configuration se fait dans `vercel.json` :\n\n```json\n{\n  \"redirects\": [\n    {\n      \"source\": \"/rss.xml\",\n      \"destination\": \"/feed.xml\",\n      \"permanent\": true\n    }\n  ]\n}\n```\n\nPour un déploiement sur Netlify, l'équivalent irait dans `netlify.toml` :\n\n```toml\n[[redirects]]\n  from = \"/rss.xml\"\n  to = \"/feed.xml\"\n  status = 301\n```\n\nDéploiement en 4 minutes. Vérification immédiate :\n\n```bash\ncurl -sI https://blog.exemple.com/rss.xml\n\nHTTP/2 301\nlocation: https://blog.exemple.com/feed.xml\n```\n\nLa redirect est en place. Mais est-ce suffisant ?\n\n### Étape 2 : garder `/rss.xml` comme URL canonique du feed\n\nL'équipe discute. Deux options :\n\n**Option A** : garder la redirect 301 de `/rss.xml` vers `/feed.xml`, et laisser `/feed.xml` comme URL principale.\n\n**Option B** : renommer le fichier Astro pour que le feed soit directement servi depuis `/rss.xml`, sans redirect.\n\nL'option B est plus propre. Moins de hops, pas de risque qu'un agrégateur refuse de suivre la 301. Le renommage est trivial :\n\n```typescript\n// src/pages/rss.xml.ts (renommé depuis feed.xml.ts)\nimport rss from '@astrojs/rss';\nimport { getCollection } from 'astro:content';\nimport type { APIContext } from 'astro';\n\nexport async function GET(context: APIContext) {\n  const posts = await getCollection('blog');\n  return rss({\n    title: 'Blog Exemple',\n    description: 'Articles techniques sur le développement web',\n    site: context.site!,\n    items: posts.map((post) => ({\n      title: post.data.title,\n      pubDate: post.data.date,\n      description: post.data.description,\n      link: `/blog/${post.slug}/`,\n    })),\n    customData: `\u003Clanguage>fr-fr\u003C/language>`,\n  });\n}\n```\n\nLe `\u003Chead>` est mis à jour en conséquence :\n\n```html\n\u003Clink\n  rel=\"alternate\"\n  type=\"application/rss+xml\"\n  title=\"Blog Exemple\"\n  href=\"https://blog.exemple.com/rss.xml\"\n/>\n```\n\nEt une redirect 301 est ajoutée dans l'autre sens, de `/feed.xml` vers `/rss.xml`, pour les rares personnes qui auraient bookmarké la nouvelle URL pendant les 6 semaines intermédiaires.\n\n### Étape 3 : réactiver les agrégateurs\n\nLa redirect seule ne suffit pas. Feedly a marqué le feed comme inactif. Il faut forcer la réactivation.\n\nL'équipe utilise l'outil de validation Feedly (`https://feedly.com/i/subscription/feed/https://blog.exemple.com/rss.xml`) pour soumettre à nouveau le feed. Feedly le re-poll dans l'heure.\n\nPour les autres agrégateurs, pas d'interface manuelle. Mais le simple fait que `/rss.xml` retourne un 200 avec un XML valide suffit. La plupart des agrégateurs retentent les feeds en erreur toutes les 24 à 72 heures avant de les désactiver définitivement. Après 43 jours de 404, certains ont probablement purgé le feed de leur base. Ces abonnés sont perdus.\n\nL'équipe envoie un email à la liste de contacts des trois newsletters tierces pour les prévenir que le feed est rétabli. Deux répondent dans la journée et réactivent le scraping. La troisième ne répond jamais.\n\n### Étape 4 : vérification et monitoring\n\nValidation du feed XML avec `xmllint` :\n\n```bash\ncurl -s https://blog.exemple.com/rss.xml | xmllint --noout -\n# Pas d'erreur = XML bien formé\n```\n\nValidation du contenu : les 20 derniers articles sont bien présents, les `\u003Clink>` pointent vers les bonnes URLs, les dates `\u003CpubDate>` sont au format RFC 822.\n\n### Temps de récupération\n\n- **J+1** : Feedly affiche à nouveau les articles. Les abonnés qui n'avaient pas désactivé le feed voient les 20 derniers articles apparaître d'un coup.\n- **J+3** : le trafic referral via agrégateurs remonte à environ 40 % du niveau pré-migration.\n- **J+14** : 75 % du niveau pré-migration. Les 25 % manquants correspondent probablement aux abonnés qui ont purgé le feed inactif de leur agrégateur et ne le réajouteront jamais.\n- **J+30** : stabilisation à environ 80 % du trafic referral RSS d'avant migration. Le reste est une perte sèche.\n\nSur les 1 200 abonnés Feedly d'origine, environ 950 ont retrouvé le feed. 250 sont partis. Pour un blog technique, c'est 250 lecteurs engagés — le type de lecteur qui partage, qui commente, qui linke. La perte n'est pas anodine.\n\n### Le checklist de migration mis à jour\n\nL'équipe ajoute trois lignes au runbook :\n\n1. **Avant migration** : lister toutes les URLs non-HTML servies par l'ancien site (RSS, Atom, JSON feed, sitemap, robots.txt, fichiers statiques critiques). Les inclure dans le crawl comparatif.\n2. **Au déploiement** : vérifier que chaque URL non-HTML de l'ancien site retourne soit un 200 soit une 301 vers l'équivalent du nouveau site.\n3. **Post-déploiement** : monitorer les status codes des URLs non-HTML pendant 30 jours minimum.\n\nCe type de régression sur les fichiers RSS touche aussi les migrations similaires. L'équipe qui migre de [Nuxt 2 vers Nuxt 3](/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines) ou de [Next.js Pages Router vers App Router](/blog/migration-next-js-pages-router-vers-app-router-les-metadata-ignorees-sur-les-pages-client) fait face au même angle mort : les fichiers auxiliaires que personne ne checke parce qu'ils ne sont pas dans le DOM visible. Le pattern se répète aussi avec les [content collections Astro qui cassent le frontmatter](/blog/astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title) — un feed RSS qui tire ses données de collections mal mappées génère un XML vide ou des titres manquants, tout aussi silencieusement.\n\nEt quand l'environnement de staging n'est pas [stress-testé correctement](/blog/how-to-stress-test-a-staging-environment-to-surface-risks-pre-launch-ask-an-seo-via-sejournal-helenpollitt1), ce genre de divergence passe systématiquement sous le radar.\n\n## Ce qu'on en retient\n\nLe RSS est un canal invisible. Pas de dashboard natif dans GA4. Pas de section dédiée dans Search Console. Pas d'alerte Lighthouse. Un feed qui tombe en 404 ne déclenche aucune notification, chez personne. Les agrégateurs abandonnent silencieusement. Les abonnés ne se plaignent pas — ils oublient.\n\nLors d'une migration de framework, chaque URL servie par l'ancien système doit être inventoriée et redirigée. Pas seulement les pages. Pas seulement le sitemap. Les feeds. Les fichiers JSON. Les endpoints API consommés par des tiers.\n\nUn monitoring continu type Seogard, qui surveille les status codes de toutes les URLs critiques — y compris les non-HTML — aurait détecté le 404 sur `/rss.xml` dans les heures suivant le déploiement. Pas six semaines plus tard, via un tweet.\n\nLa prochaine migration, le RSS sera sur la checklist. Mais combien d'autres fichiers fantômes attendent dans l'ombre ?\n```","https://seogard.io/blog/migration-gatsby-vers-astro-rss-feed-orphelin-pendant-6-semaines","Migration","2026-05-26T06:01:59.887Z","2026-05-26","Un blog tech migre de Gatsby vers Astro. Le RSS feed change d'URL sans redirect. 6 semaines de silence. Récit, diagnostic et fix complet.","\u003Ch1>Migration Gatsby vers Astro : un RSS feed orphelin pendant 6 semaines\u003C/h1>\n\u003Cp>Mardi 14 janvier, 22h30. L'équipe contenu d'un éditeur SaaS français (blog technique, 380 articles, 28 000 visites organiques/mois) célèbre le déploiement du nouveau blog. Gatsby 4 est mort, Astro 4.x tourne en production. Le Lighthouse affiche 98. Le design est propre. Tout le monde rentre. Personne ne pense au fichier \u003Ccode>/rss.xml\u003C/code>. Six semaines plus tard, un développeur advocate remarque que Feedly affiche \"Feed not found\" sur le blog. L'investigation commence.\u003C/p>\n\u003Ch2>Mercredi 26 février, 9h12 — \"Plus personne ne nous lit ?\"\u003C/h2>\n\u003Cp>L'alerte ne vient pas de Search Console. Pas de Slack bot. Pas de monitoring. Elle vient d'un tweet.\u003C/p>\n\u003Cblockquote>\n\u003Cp>\"Hey @[marque], votre RSS est cassé depuis un moment, Feedly me renvoie une erreur.\"\u003C/p>\n\u003C/blockquote>\n\u003Cp>Le dev advocate forward le message au lead contenu. Première réaction : \"On a un RSS ?\" Oui. Gatsby le générait via \u003Ccode>gatsby-plugin-feed\u003C/code>. L'URL était \u003Ccode>/rss.xml\u003C/code>. Chaque article publié apparaissait dans le feed, et une poignée de newsletters tierces, trois agrégateurs sectoriels et environ 1 200 abonnés Feedly tiraient ce flux automatiquement.\u003C/p>\n\u003Cp>Le lead contenu ouvre \u003Ccode>https://blog.exemple.com/rss.xml\u003C/code> dans un navigateur. Réponse : 404.\u003C/p>\n\u003Cp>Il essaie \u003Ccode>/feed.xml\u003C/code>. Réponse : un XML valide. Astro sert le feed depuis \u003Ccode>@astrojs/rss\u003C/code>, et le fichier de sortie par défaut est \u003Ccode>feed.xml\u003C/code>, pas \u003Ccode>rss.xml\u003C/code>.\u003C/p>\n\u003Cp>Premier réflexe : vérifier depuis combien de temps le problème existe. Le déploiement Astro date du 14 janvier. On est le 26 février. 43 jours.\u003C/p>\n\u003Cp>Le lead contenu tire les chiffres. Dans GA4, le segment \"referral\" filtré sur les domaines connus d'agrégateurs (feedly.com, inoreader.com, newsblur.com, theoldreader.com) montre un effondrement net :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>Décembre\u003C/strong> (dernier mois complet sous Gatsby) : 3 840 sessions referral via agrégateurs RSS.\u003C/li>\n\u003Cli>\u003Cstrong>Janvier\u003C/strong> (bascule le 14) : 2 100 sessions. La moitié du mois fonctionnait encore.\u003C/li>\n\u003Cli>\u003Cstrong>Février\u003C/strong> (au 26) : 310 sessions.\u003C/li>\n\u003C/ul>\n\u003Cp>L'inbound RSS représentait 14 % du trafic referral total du blog. Ce canal est tombé à quasi-zéro sans que personne ne le remarque. Aucune alerte. Aucun dashboard ne suivait ce flux. L'équipe SEO surveillait les positions organiques et les Core Web Vitals. Le RSS n'existait dans aucun runbook de migration.\u003C/p>\n\u003Cp>Le CTO demande un état des lieux complet. Combien de sources externes pointent encore vers \u003Ccode>/rss.xml\u003C/code> ? Un crawl rapide via Screaming Frog sur le backlink profile (export Ahrefs) révèle 17 liens entrants pointant explicitement vers \u003Ccode>/rss.xml\u003C/code>. Trois newsletters sectorielles. Deux portails de curation. Le reste : des pages \"blogroll\" de développeurs individuels.\u003C/p>\n\u003Cp>Tous ces liens renvoient désormais un 404. Les agrégateurs ont progressivement abandonné le polling. Feedly, après quelques tentatives en erreur, marque le feed comme inactif. Les 1 200 abonnés ne reçoivent plus rien. Ils ne le savent pas. Ils n'ont reçu aucune notification. Le feed a simplement cessé de se mettre à jour dans leur interface.\u003C/p>\n\u003Cp>Personne n'a crié. C'est le problème des régressions RSS : les utilisateurs ne reviennent pas se plaindre. Ils oublient.\u003C/p>\n\u003Ch2>Le bug : deux URLs, zéro redirect, un fichier fantôme\u003C/h2>\n\u003Cp>Pour comprendre comment c'est arrivé, il faut remonter à la configuration des deux stacks.\u003C/p>\n\u003Ch3>Côté Gatsby : le feed historique\u003C/h3>\n\u003Cp>Dans \u003Ccode>gatsby-config.js\u003C/code>, le plugin RSS était configuré ainsi :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// gatsby-config.js (Gatsby 4)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">module\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#79B8FF\">exports\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  plugins: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      resolve: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`gatsby-plugin-feed`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      options: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        query: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            site {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">              siteMetadata {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                title\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                description\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                siteUrl\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">              }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">          }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">        `\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        feeds: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">            serialize\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: ({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">query\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: { \u003C/span>\u003Cspan style=\"color:#FFAB70\">site\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">allMarkdownRemark\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\">              return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> allMarkdownRemark.nodes.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">node\u003C/span>\u003Cspan style=\"color:#F97583\"> =>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                title: node.frontmatter.title,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                description: node.excerpt,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                date: node.frontmatter.date,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                url: site.siteMetadata.siteUrl \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> node.fields.slug,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">                guid: site.siteMetadata.siteUrl \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> node.fields.slug,\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\">            query: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">              {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                allMarkdownRemark(sort: {frontmatter: {date: DESC}}) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                  nodes {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                    excerpt\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                    fields { slug }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                    frontmatter { title date }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">                }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">              }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">            `\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            output: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/rss.xml\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">            title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Blog Exemple — RSS Feed\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">          },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Le point clé : \u003Ccode>output: \"/rss.xml\"\u003C/code>. C'est cette URL que les agrégateurs connaissent. C'est cette URL qui est linkée dans les \u003Ccode>&#x3C;link rel=\"alternate\">\u003C/code> du \u003Ccode>&#x3C;head>\u003C/code> HTML. C'est cette URL que 1 200 personnes ont collée dans Feedly.\u003C/p>\n\u003Ch3>Côté Astro : le nouveau feed\u003C/h3>\n\u003Cp>L'équipe installe \u003Ccode>@astrojs/rss\u003C/code> et crée le endpoint selon la documentation officielle :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/pages/feed.xml.ts (Astro 4.x)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rss \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@astrojs/rss'\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\"> { getCollection } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:content'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { APIContext } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> GET\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">context\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> APIContext\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\"> posts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCollection\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'blog'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#B392F0\"> rss\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Blog Exemple'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Articles techniques sur le développement web'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    site: context.site\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    items: posts.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">post\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\">      title: post.data.title,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      pubDate: post.data.date,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      description: post.data.description,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      link: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/blog/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">post\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    })),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    customData: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`&#x3C;language>fr-fr&#x3C;/language>`\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>Le fichier s'appelle \u003Ccode>feed.xml.ts\u003C/code>. Astro génère donc la route \u003Ccode>/feed.xml\u003C/code>. Pas \u003Ccode>/rss.xml\u003C/code>.\u003C/p>\n\u003Cp>La documentation d'Astro utilise \u003Ccode>feed.xml\u003C/code> dans ses exemples. Le développeur qui a configuré le RSS a suivi la doc. Il n'a pas pensé à vérifier l'URL de l'ancien feed. L'ancien feed n'était documenté nulle part dans le runbook de migration.\u003C/p>\n\u003Ch3>Ce que voit le navigateur vs ce que voient les agrégateurs\u003C/h3>\n\u003Cp>Un humain qui visite le blog ne voit rien de cassé. Le \u003Ccode>&#x3C;head>\u003C/code> du nouveau site contient bien un lien vers le feed — mais vers le \u003Cem>nouveau\u003C/em> :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">&#x3C;!-- &#x3C;head> du site Astro en production -->\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">&#x3C;\u003C/span>\u003Cspan style=\"color:#85E89D\">link\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  type\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"application/rss+xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Blog Exemple\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://blog.exemple.com/feed.xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">/>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Aucun problème pour un nouveau visiteur qui découvre le blog et cherche le RSS. Mais les 1 200 abonnés existants ne revisitent pas la page d'accueil pour trouver le nouveau lien. Leur agrégateur continue de poller \u003Ccode>/rss.xml\u003C/code>, qui renvoie un 404.\u003C/p>\n\u003Cp>Les newsletters tierces qui scrappent \u003Ccode>/rss.xml\u003C/code> automatiquement se retrouvent face au même 404. Après 3 à 7 jours de 404 consécutifs (selon la politique de retry de chaque agrégateur), la plupart arrêtent le polling. Feedly affiche un badge \"inactive\" après environ 48h de 404. Certains outils comme Inoreader persistent un peu plus longtemps, mais finissent par décrocher aussi.\u003C/p>\n\u003Ch3>Pourquoi les tests n'ont rien détecté\u003C/h3>\n\u003Cp>Le checklist de migration contenait 34 points. Voici ce qui était vérifié :\u003C/p>\n\u003Cul>\n\u003Cli>Redirections 301 de toutes les anciennes URLs d'articles : ✅\u003C/li>\n\u003Cli>Sitemap XML présent et soumis à Search Console : ✅\u003C/li>\n\u003Cli>Canonical tags corrects : ✅\u003C/li>\n\u003Cli>Meta robots : ✅\u003C/li>\n\u003Cli>Core Web Vitals post-déploiement : ✅\u003C/li>\n\u003Cli>RSS feed : absent de la checklist.\u003C/li>\n\u003C/ul>\n\u003Cp>Le crawl Screaming Frog pré-déploiement était configuré pour scanner les URLs du sitemap + les liens internes. Le RSS feed n'apparaissait dans aucun sitemap (c'est normal, les fichiers RSS ne sont pas indexés). Il n'était linké en \u003Ccode>&#x3C;a href>\u003C/code> nulle part dans le body des pages — uniquement en \u003Ccode>&#x3C;link rel=\"alternate\">\u003C/code> dans le \u003Ccode>&#x3C;head>\u003C/code>.\u003C/p>\n\u003Cp>Screaming Frog, par défaut, crawle les \u003Ccode>&#x3C;link>\u003C/code> du \u003Ccode>&#x3C;head>\u003C/code>. Mais le crawl de validation a été lancé \u003Cem>après\u003C/em> le déploiement Astro. Le nouveau \u003Ccode>&#x3C;head>\u003C/code> pointe vers \u003Ccode>/feed.xml\u003C/code>, qui répond 200. Le crawl est vert. L'ancien \u003Ccode>/rss.xml\u003C/code> n'est référencé nulle part sur le nouveau site. Il n'apparaît donc jamais dans le crawl.\u003C/p>\n\u003Cp>Le problème fondamental : \u003Cstrong>la régression se situe dans la rupture d'une URL externe qui n'existe plus dans le périmètre du nouveau site.\u003C/strong> Les outils de crawl vérifient ce qui existe. Pas ce qui a disparu.\u003C/p>\n\u003Cp>Pour détecter cette régression, il aurait fallu un diff entre les URLs servies par l'ancien site et celles servies par le nouveau. Un crawl comparatif. Ou un monitoring des URLs critiques non-HTML (RSS, sitemap, robots.txt, fichiers JSON-LD standalone) avec alerting sur changement de status code.\u003C/p>\n\u003Ch3>L'effet cascade sur le SEO indirect\u003C/h3>\n\u003Cp>Le RSS feed ne contribue pas directement au ranking Google. Googlebot ne crawle pas les feeds RSS pour indexer les pages (il utilise le sitemap et les liens internes). Mais le RSS avait un effet indirect mesurable :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Trois newsletters sectorielles\u003C/strong> tiraient leur contenu du feed. Chaque article publié générait un lien entrant depuis l'archive de la newsletter. Depuis le 14 janvier, zéro nouveau lien entrant via ce canal.\u003C/li>\n\u003Cli>\u003Cstrong>Deux portails de curation technique\u003C/strong> affichaient les derniers articles du blog via le RSS. Les articles ont disparu de ces portails. Perte de visibilité indirecte.\u003C/li>\n\u003Cli>\u003Cstrong>Les abonnés Feedly\u003C/strong> partageaient régulièrement les articles sur Twitter/LinkedIn. Ce relais social s'est tari.\u003C/li>\n\u003C/ol>\n\u003Cp>Le trafic referral RSS (3 800 sessions/mois) n'est pas du trafic organique. Mais la perte de liens entrants générés par les newsletters a un impact organique différé. L'équipe observe, sur les 6 semaines, une baisse de 8 positions moyennes sur 12 mots-clés informationnels à forte concurrence. Corrélation, pas causalité prouvée — mais le timing coïncide.\u003C/p>\n\u003Ch2>Le fix : redirect, alias, et récupération\u003C/h2>\n\u003Ch3>Étape 1 : la redirect 301 immédiate\u003C/h3>\n\u003Cp>Le fix le plus urgent : rediriger \u003Ccode>/rss.xml\u003C/code> vers \u003Ccode>/feed.xml\u003C/code>. Sur Astro déployé via Vercel (ce qui est le cas ici), la configuration se fait dans \u003Ccode>vercel.json\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:#E1E4E8\">{\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  \"redirects\"\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:#79B8FF\">      \"source\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/rss.xml\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"destination\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/feed.xml\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">      \"permanent\"\u003C/span>\u003Cspan style=\"color:#E1E4E8\">: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  ]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Pour un déploiement sur Netlify, l'équivalent irait dans \u003Ccode>netlify.toml\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:#E1E4E8\">[[\u003C/span>\u003Cspan style=\"color:#B392F0\">redirects\u003C/span>\u003Cspan style=\"color:#E1E4E8\">]]\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  from = \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/rss.xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  to = \u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"/feed.xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  status = \u003C/span>\u003Cspan style=\"color:#79B8FF\">301\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Déploiement en 4 minutes. Vérification immédiate :\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\"> -sI\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://blog.exemple.com/rss.xml\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">HTTP/2\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 301\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">location:\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://blog.exemple.com/feed.xml\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>La redirect est en place. Mais est-ce suffisant ?\u003C/p>\n\u003Ch3>Étape 2 : garder \u003Ccode>/rss.xml\u003C/code> comme URL canonique du feed\u003C/h3>\n\u003Cp>L'équipe discute. Deux options :\u003C/p>\n\u003Cp>\u003Cstrong>Option A\u003C/strong> : garder la redirect 301 de \u003Ccode>/rss.xml\u003C/code> vers \u003Ccode>/feed.xml\u003C/code>, et laisser \u003Ccode>/feed.xml\u003C/code> comme URL principale.\u003C/p>\n\u003Cp>\u003Cstrong>Option B\u003C/strong> : renommer le fichier Astro pour que le feed soit directement servi depuis \u003Ccode>/rss.xml\u003C/code>, sans redirect.\u003C/p>\n\u003Cp>L'option B est plus propre. Moins de hops, pas de risque qu'un agrégateur refuse de suivre la 301. Le renommage est trivial :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// src/pages/rss.xml.ts (renommé depuis feed.xml.ts)\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> rss \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> '@astrojs/rss'\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\"> { getCollection } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro:content'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { APIContext } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'astro'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> GET\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">context\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> APIContext\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\"> posts\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#B392F0\"> getCollection\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'blog'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#B392F0\"> rss\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    title: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Blog Exemple'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    description: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'Articles techniques sur le développement web'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    site: context.site\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    items: posts.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">post\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\">      title: post.data.title,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      pubDate: post.data.date,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      description: post.data.description,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      link: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`/blog/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">post\u003C/span>\u003Cspan style=\"color:#9ECBFF\">.\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}/`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    })),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    customData: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">`&#x3C;language>fr-fr&#x3C;/language>`\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>Le \u003Ccode>&#x3C;head>\u003C/code> est mis à jour en conséquence :\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>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  rel\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"alternate\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  type\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"application/rss+xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  title\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"Blog Exemple\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  href\u003C/span>\u003Cspan style=\"color:#E1E4E8\">=\u003C/span>\u003Cspan style=\"color:#9ECBFF\">\"https://blog.exemple.com/rss.xml\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">/>\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Et une redirect 301 est ajoutée dans l'autre sens, de \u003Ccode>/feed.xml\u003C/code> vers \u003Ccode>/rss.xml\u003C/code>, pour les rares personnes qui auraient bookmarké la nouvelle URL pendant les 6 semaines intermédiaires.\u003C/p>\n\u003Ch3>Étape 3 : réactiver les agrégateurs\u003C/h3>\n\u003Cp>La redirect seule ne suffit pas. Feedly a marqué le feed comme inactif. Il faut forcer la réactivation.\u003C/p>\n\u003Cp>L'équipe utilise l'outil de validation Feedly (\u003Ccode>https://feedly.com/i/subscription/feed/https://blog.exemple.com/rss.xml\u003C/code>) pour soumettre à nouveau le feed. Feedly le re-poll dans l'heure.\u003C/p>\n\u003Cp>Pour les autres agrégateurs, pas d'interface manuelle. Mais le simple fait que \u003Ccode>/rss.xml\u003C/code> retourne un 200 avec un XML valide suffit. La plupart des agrégateurs retentent les feeds en erreur toutes les 24 à 72 heures avant de les désactiver définitivement. Après 43 jours de 404, certains ont probablement purgé le feed de leur base. Ces abonnés sont perdus.\u003C/p>\n\u003Cp>L'équipe envoie un email à la liste de contacts des trois newsletters tierces pour les prévenir que le feed est rétabli. Deux répondent dans la journée et réactivent le scraping. La troisième ne répond jamais.\u003C/p>\n\u003Ch3>Étape 4 : vérification et monitoring\u003C/h3>\n\u003Cp>Validation du feed XML avec \u003Ccode>xmllint\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:#B392F0\">curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> https://blog.exemple.com/rss.xml\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#B392F0\"> xmllint\u003C/span>\u003Cspan style=\"color:#79B8FF\"> --noout\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> -\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Pas d'erreur = XML bien formé\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Validation du contenu : les 20 derniers articles sont bien présents, les \u003Ccode>&#x3C;link>\u003C/code> pointent vers les bonnes URLs, les dates \u003Ccode>&#x3C;pubDate>\u003C/code> sont au format RFC 822.\u003C/p>\n\u003Ch3>Temps de récupération\u003C/h3>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+1\u003C/strong> : Feedly affiche à nouveau les articles. Les abonnés qui n'avaient pas désactivé le feed voient les 20 derniers articles apparaître d'un coup.\u003C/li>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : le trafic referral via agrégateurs remonte à environ 40 % du niveau pré-migration.\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : 75 % du niveau pré-migration. Les 25 % manquants correspondent probablement aux abonnés qui ont purgé le feed inactif de leur agrégateur et ne le réajouteront jamais.\u003C/li>\n\u003Cli>\u003Cstrong>J+30\u003C/strong> : stabilisation à environ 80 % du trafic referral RSS d'avant migration. Le reste est une perte sèche.\u003C/li>\n\u003C/ul>\n\u003Cp>Sur les 1 200 abonnés Feedly d'origine, environ 950 ont retrouvé le feed. 250 sont partis. Pour un blog technique, c'est 250 lecteurs engagés — le type de lecteur qui partage, qui commente, qui linke. La perte n'est pas anodine.\u003C/p>\n\u003Ch3>Le checklist de migration mis à jour\u003C/h3>\n\u003Cp>L'équipe ajoute trois lignes au runbook :\u003C/p>\n\u003Col>\n\u003Cli>\u003Cstrong>Avant migration\u003C/strong> : lister toutes les URLs non-HTML servies par l'ancien site (RSS, Atom, JSON feed, sitemap, robots.txt, fichiers statiques critiques). Les inclure dans le crawl comparatif.\u003C/li>\n\u003Cli>\u003Cstrong>Au déploiement\u003C/strong> : vérifier que chaque URL non-HTML de l'ancien site retourne soit un 200 soit une 301 vers l'équivalent du nouveau site.\u003C/li>\n\u003Cli>\u003Cstrong>Post-déploiement\u003C/strong> : monitorer les status codes des URLs non-HTML pendant 30 jours minimum.\u003C/li>\n\u003C/ol>\n\u003Cp>Ce type de régression sur les fichiers RSS touche aussi les migrations similaires. L'équipe qui migre de \u003Ca href=\"/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines\">Nuxt 2 vers Nuxt 3\u003C/a> ou de \u003Ca href=\"/blog/migration-next-js-pages-router-vers-app-router-les-metadata-ignorees-sur-les-pages-client\">Next.js Pages Router vers App Router\u003C/a> fait face au même angle mort : les fichiers auxiliaires que personne ne checke parce qu'ils ne sont pas dans le DOM visible. Le pattern se répète aussi avec les \u003Ca href=\"/blog/astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title\">content collections Astro qui cassent le frontmatter\u003C/a> — un feed RSS qui tire ses données de collections mal mappées génère un XML vide ou des titres manquants, tout aussi silencieusement.\u003C/p>\n\u003Cp>Et quand l'environnement de staging n'est pas \u003Ca href=\"/blog/how-to-stress-test-a-staging-environment-to-surface-risks-pre-launch-ask-an-seo-via-sejournal-helenpollitt1\">stress-testé correctement\u003C/a>, ce genre de divergence passe systématiquement sous le radar.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Le RSS est un canal invisible. Pas de dashboard natif dans GA4. Pas de section dédiée dans Search Console. Pas d'alerte Lighthouse. Un feed qui tombe en 404 ne déclenche aucune notification, chez personne. Les agrégateurs abandonnent silencieusement. Les abonnés ne se plaignent pas — ils oublient.\u003C/p>\n\u003Cp>Lors d'une migration de framework, chaque URL servie par l'ancien système doit être inventoriée et redirigée. Pas seulement les pages. Pas seulement le sitemap. Les feeds. Les fichiers JSON. Les endpoints API consommés par des tiers.\u003C/p>\n\u003Cp>Un monitoring continu type Seogard, qui surveille les status codes de toutes les URLs critiques — y compris les non-HTML — aurait détecté le 404 sur \u003Ccode>/rss.xml\u003C/code> dans les heures suivant le déploiement. Pas six semaines plus tard, via un tweet.\u003C/p>\n\u003Cp>La prochaine migration, le RSS sera sur la checklist. Mais combien d'autres fichiers fantômes attendent dans l'ombre ?\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,11,[18,19,20,21],"gatsby","astro","rss","migration","Migration Gatsby → Astro : RSS feed orphelin, 6 semaines","Tue May 26 2026 06:01:59 GMT+0000 (Coordinated Universal Time)",[25,41,53],{"_id":26,"slug":27,"__v":6,"author":7,"canonical":28,"category":10,"createdAt":29,"date":30,"description":31,"image":15,"imageAlt":15,"readingTime":32,"tags":33,"title":39,"updatedAt":40},"6a141e10aa6b273b0c8a4eb7","react-17-vers-react-18-suspense-ssr-fait-crasher-next-head-en-streaming","https://seogard.io/blog/react-17-vers-react-18-suspense-ssr-fait-crasher-next-head-en-streaming","2026-05-25T10:01:52.337Z","2026-05-25","Migration React 17→18 : le streaming SSR réordonne les chunks et supprime les meta tags. Récit d'incident, diagnostic complet et patch Next.js.",12,[34,35,36,37,38],"react 18","suspense","ssr","next/head","streaming","React 18 Suspense SSR : next/head cassé par le streaming","Mon May 25 2026 10:01:52 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":30,"description":46,"image":15,"imageAlt":15,"readingTime":16,"tags":47,"title":51,"updatedAt":52},"6a14645baa6b273b0cc45458","astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title","https://seogard.io/blog/astro-v5-vers-v6-content-collections-cassent-le-mapping-frontmatter-title","2026-05-25T15:01:47.013Z","Après upgrade Astro v5→v6, 312 articles perdent leur balise title. Récit du bug, diagnostic frontmatter, fix et récupération SEO en 19 jours.",[19,48,49,50,21],"content collections","frontmatter","title","Astro v6 : Content Collections cassent les title en silence","Mon May 25 2026 15:01:47 GMT+0000 (Coordinated Universal Time)",{"_id":54,"slug":55,"__v":6,"author":7,"canonical":56,"category":10,"createdAt":57,"date":30,"description":58,"image":15,"imageAlt":15,"readingTime":32,"tags":59,"title":63,"updatedAt":64},"6a148e95aa6b273b0ce72f4a","migration-angular-17-vers-ssr-provideserverrendering-mal-configure-et-hydration-mismatch-invisible","https://seogard.io/blog/migration-angular-17-vers-ssr-provideserverrendering-mal-configure-et-hydration-mismatch-invisible","2026-05-25T18:01:57.093Z","Migration Angular 17 vers SSR : provideServerRendering mal configuré cause un hydration mismatch invisible. Récit, diagnostic Lighthouse, fix précis.",[60,36,61,62],"angular 17","hydration","provideServerRendering","Angular 17 SSR : hydration mismatch invisible, −34 % trafic","Mon May 25 2026 18:01:57 GMT+0000 (Coordinated Universal Time)"]