[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fkHJP7zZBnroAdfj83hlqRomBucIXftpz0kS53OqGXHE":3,"$fWnIN3AoOv2NmGN_7Z_9kNW4eqVG1UnF611dKAcsnplU":25},{"_id":4,"slug":5,"__v":6,"author":7,"body":8,"canonical":9,"category":10,"createdAt":11,"date":12,"description":13,"htmlContent":14,"image":15,"imageAlt":15,"readingTime":16,"tags":17,"title":23,"updatedAt":24},"6a1d2048aa6b273b0cfa9382","migration-vercel-vers-railway-perte-du-edge-isr-ttfb-multiplie-par-4",0,"Equipe Seogard","# Migration Vercel vers Railway : 2000 pages perdent l'Edge ISR, le TTFB explose\n\nJeudi 14h. L'équipe infra d'une marketplace B2B française — 2 100 pages catalogue, 380K sessions organiques par mois — finalise la migration de Vercel vers Railway. Motivation : réduire la facture d'hébergement de 40 %. Le déploiement passe. Les tests Cypress sont verts. Le DNS bascule à 16h. Vendredi soir, tout semble normal. Lundi matin, le rapport CrUX tombe. Le TTFB médian est passé de 340 ms à 1,4 seconde. Sur mobile, le LCP dépasse 4 secondes sur 73 % des pages produit.\n\n## Lundi 9h12 — Le rapport CrUX qui ne ment pas\n\nC'est le lead SEO qui tire la sonnette. Il consulte le rapport PageSpeed Insights sur une page catégorie clé. Le score Performance mobile est tombé de 82 à 41. Le TTFB est rouge. Le LCP aussi. Le CLS est resté stable — ce n'est donc pas un problème de layout.\n\nIl ouvre Search Console. Pas encore de signal d'alerte côté indexation — trop tôt. Mais le rapport Core Web Vitals montre déjà une migration de URLs du bucket \"Bon\" vers \"À améliorer\" sur les données de terrain.\n\nÀ 9h35, il envoie un message au CTO : \"On a un problème de perf massif depuis la migration. Le TTFB a triplé au minimum.\"\n\nLe CTO répond vite : \"Impossible, le déploiement Railway est identique. Même image Docker, même région eu-west.\"\n\nPremière hypothèse — la base de données. L'équipe vérifie les temps de réponse Postgres sur Railway. Le P95 est à 18 ms. Rien d'anormal. Les requêtes Prisma sont profilées : aucune régression.\n\nDeuxième hypothèse — le CDN. Sur Vercel, le CDN edge était intégré. Sur Railway, l'équipe a configuré Cloudflare en proxy. Mais les headers `cf-cache-status` montrent des `HIT` sur les assets statiques. Le problème n'est pas là.\n\nÀ 10h20, le développeur frontend senior tape une commande qui change tout :\n\n```bash\ncurl -s -o /dev/null -w \"TTFB: %{time_starttransfer}s\\nTotal: %{time_total}s\\nHTTP: %{http_code}\\n\" \\\n  -H \"User-Agent: Googlebot\" \\\n  https://marketplace.example.com/produit/cable-hdmi-4k-2m\n```\n\nRésultat :\n\n```\nTTFB: 1.387s\nTotal: 1.412s\nHTTP: 200\n```\n\nMême test sur le même slug, mais via le cache Wayback de la version Vercel d'il y a une semaine : TTFB de 87 ms. Le ratio est de ×16, pas ×4. Le ×4 est la médiane globale. Les pages dynamiques individuelles sont bien pires.\n\nC'est à ce moment que l'équipe comprend : le problème n'est pas un ralentissement réseau. C'est l'absence totale de cache ISR. Chaque requête déclenche un rendu SSR complet.\n\n## Le bug : quand l'Edge ISR disparaît sans laisser de trace\n\nPour comprendre ce qui s'est passé, il faut revenir à l'architecture en place sur Vercel.\n\n### La configuration Vercel d'origine\n\nLe site tournait sur Next.js 14.2 avec l'App Router. Les pages produit utilisaient `generateStaticParams` pour le pré-rendu, combiné à `revalidate` pour l'ISR :\n\n```typescript\n// app/produit/[slug]/page.tsx — version Vercel\n\nexport const revalidate = 3600; // ISR : revalidation toutes les heures\n\nexport async function generateStaticParams() {\n  const products = await prisma.product.findMany({\n    select: { slug: true },\n  });\n  return products.map((p) => ({ slug: p.slug }));\n}\n\nexport default async function ProductPage({ params }: { params: { slug: string } }) {\n  const product = await prisma.product.findUnique({\n    where: { slug: params.slug },\n    include: { category: true, specs: true },\n  });\n\n  if (!product) notFound();\n\n  return (\n    \u003C>\n      \u003CProductJsonLd product={product} />\n      \u003CProductDetail product={product} />\n    \u003C/>\n  );\n}\n```\n\nSur Vercel, ce code produisait un comportement précis :\n\n1. Au build, `generateStaticParams` pré-rendait les 2 100 pages en HTML statique.\n2. Les pages étaient servies depuis l'edge CDN de Vercel, avec un TTFB de 40 à 90 ms.\n3. Après 3 600 secondes, une requête entrante déclenchait une revalidation en arrière-plan. L'ancienne version restait servie pendant la régénération — c'est le principe du stale-while-revalidate.\n4. Les Edge Functions de Vercel géraient cette logique nativement, sans configuration supplémentaire.\n\nLes headers de réponse sur Vercel ressemblaient à ceci :\n\n```\nx-vercel-cache: HIT\ncache-control: s-maxage=3600, stale-while-revalidate\nx-nextjs-cache: HIT\n```\n\n### Ce qui se passe sur Railway\n\nRailway exécute Next.js en mode `standalone` via `next start`. L'image Docker contient le serveur Node.js, point final. Il n'y a pas d'edge CDN intégré. Pas de couche de cache ISR persistante entre le serveur et le client.\n\nLe `next.config.js` de l'équipe :\n\n```javascript\n// next.config.js — version Railway\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  output: 'standalone',\n  images: {\n    remotePatterns: [\n      { protocol: 'https', hostname: 'cdn.marketplace.example.com' },\n    ],\n  },\n};\n\nmodule.exports = nextConfig;\n```\n\nAvec `output: 'standalone'`, Next.js génère un serveur Node minimal. Mais ce serveur n'a **aucun mécanisme de cache ISR intégré**. Le répertoire `.next/cache` qui stocke les pages ISR sur Vercel n'est pas persisté entre les redéploiements sur Railway. Pire : même à l'intérieur d'un même déploiement, le cache ISR in-memory est perdu à chaque restart du container.\n\nRésultat : chaque requête sur `/produit/cable-hdmi-4k-2m` déclenche un rendu SSR complet. Appel Prisma. Rendu React. Sérialisation HTML. À chaque fois. Pour chaque visiteur. Pour chaque crawl Googlebot.\n\n### Ce que voit le développeur vs ce que voit Googlebot\n\nLe développeur teste dans Chrome. La page se charge en 600 ms environ — le navigateur est à Paris, le serveur Railway aussi, et le navigateur a du cache local sur les fonts et le CSS.\n\nGooglebot n'a pas ce luxe. Il crawle depuis des IPs américaines (principalement). Il n'a pas de cache local. Il exécute chaque requête à froid. Et il reçoit un TTFB de 1,2 à 1,8 seconde selon la complexité de la page.\n\nPour vérifier, l'équipe utilise Chrome DevTools en throttling \"Slow 3G\" et en mode Incognito — ça se rapproche de l'expérience réelle, mais pas complètement. Le vrai diagnostic vient de l'outil d'inspection d'URL de Search Console, qui affiche le HTML rendu tel que Googlebot le reçoit, avec le temps de chargement.\n\nSur 2 100 pages produit, Screaming Frog en mode rendu JavaScript révèle des temps de réponse moyens de 1 340 ms, contre 280 ms relevés une semaine avant la migration sur un crawl archivé.\n\n### Pourquoi les tests CI n'ont rien détecté\n\nL'équipe avait des tests Cypress end-to-end. Mais ces tests vérifient le contenu rendu, pas la performance de rendu. Un `cy.get('[data-testid=\"product-title\"]').should('exist')` passe en 1,4 seconde comme en 90 ms.\n\nLes tests Lighthouse CI étaient configurés pour tourner sur un environnement de staging Railway. Le staging affichait des scores corrects — parce qu'il n'avait que 50 produits en base, pas 2 100. Le SSR de 50 pages sans cache ne produit pas la même charge qu'un SSR de 2 100 pages sous trafic réel.\n\nAucun test ne comparait le TTFB avant/après migration. Aucun test ne vérifiait la présence des headers de cache ISR. C'est un angle mort classique des pipelines CI pour le SEO technique — un problème détaillé dans [cet article sur le stress testing des environnements de staging](/blog/how-to-stress-test-a-staging-environment-to-surface-risks-pre-launch-ask-an-seo-via-sejournal-helenpollitt1).\n\nLe parallèle avec d'autres migrations est frappant. Comme lors d'un [passage de Cloudflare vers Bunny CDN](/blog/migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees), c'est la couche d'infrastructure \"invisible\" qui casse silencieusement — pas le code applicatif.\n\n## Le fix : reconstruire une couche de cache ISR sans Vercel\n\nL'équipe a exploré trois options. La première — revenir sur Vercel — a été écartée pour des raisons budgétaires. La deuxième — implémenter un cache ISR custom avec Redis — a été retenue. La troisième — passer à un export statique complet — était incompatible avec les besoins de personnalisation en temps réel.\n\n### Le patch : cache ISR via Redis sur Railway\n\nRailway supporte nativement les instances Redis. L'équipe a déployé un plugin Redis et configuré un [cache handler custom pour Next.js](https://nextjs.org/docs/app/building-your-application/deploying#caching-and-isr), disponible depuis Next.js 14.1.\n\nÉtape 1 — Le cache handler custom :\n\n```typescript\n// lib/cache-handler.ts\n\nimport { CacheHandler } from 'next/dist/server/lib/incremental-cache';\nimport Redis from 'ioredis';\n\nconst redis = new Redis(process.env.REDIS_URL!);\nconst CACHE_PREFIX = 'next-isr:';\n\nexport default class RedisCacheHandler extends CacheHandler {\n  constructor(options: any) {\n    super(options);\n  }\n\n  async get(key: string) {\n    const data = await redis.get(`${CACHE_PREFIX}${key}`);\n    if (!data) return null;\n\n    const parsed = JSON.parse(data);\n    return {\n      value: parsed.value,\n      lastModified: parsed.lastModified,\n    };\n  }\n\n  async set(key: string, data: any, ctx: { revalidate?: number | false }) {\n    const payload = JSON.stringify({\n      value: data,\n      lastModified: Date.now(),\n    });\n\n    if (ctx.revalidate && typeof ctx.revalidate === 'number') {\n      // TTL = revalidate + 60s de grâce pour le stale-while-revalidate\n      await redis.setex(`${CACHE_PREFIX}${key}`, ctx.revalidate + 60, payload);\n    } else {\n      await redis.set(`${CACHE_PREFIX}${key}`, payload);\n    }\n  }\n\n  async revalidateTag(tag: string) {\n    const keys = await redis.keys(`${CACHE_PREFIX}*`);\n    // Implémentation simplifiée — en prod, utiliser un index par tag\n    for (const key of keys) {\n      await redis.del(key);\n    }\n  }\n}\n```\n\nÉtape 2 — La configuration Next.js mise à jour :\n\n```javascript\n// next.config.js — version Railway avec cache handler\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  output: 'standalone',\n  cacheHandler: require.resolve('./lib/cache-handler.ts'),\n  cacheMaxMemorySize: 0, // Désactiver le cache in-memory, tout passe par Redis\n  images: {\n    remotePatterns: [\n      { protocol: 'https', hostname: 'cdn.marketplace.example.com' },\n    ],\n  },\n};\n\nmodule.exports = nextConfig;\n```\n\nÉtape 3 — Ajout de headers cache-control via le middleware Cloudflare pour que le CDN en amont serve du stale-while-revalidate :\n\n```typescript\n// middleware.ts\n\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  const response = NextResponse.next();\n\n  if (request.nextUrl.pathname.startsWith('/produit/')) {\n    response.headers.set(\n      'Cache-Control',\n      's-maxage=3600, stale-while-revalidate=86400'\n    );\n    response.headers.set('CDN-Cache-Control', 's-maxage=3600');\n  }\n\n  return response;\n}\n\nexport const config = {\n  matcher: '/produit/:slug*',\n};\n```\n\n### Le redéploiement\n\nLe déploiement a eu lieu mercredi à 11h. L'équipe a ensuite déclenché un crawl ciblé via `curl` sur les 200 pages les plus trafiquées pour chauffer le cache Redis :\n\n```bash\n# warm-cache.sh — pré-chauffage des pages critiques\ncat top-200-slugs.txt | while read slug; do\n  curl -s -o /dev/null -w \"%{url_effective} → TTFB: %{time_starttransfer}s\\n\" \\\n    \"https://marketplace.example.com/produit/${slug}\"\n  sleep 0.2\ndone\n```\n\nRésultat immédiat après warm-up : TTFB médian retombé à 120 ms sur les pages cachées. Les pages non-cachées (premières requêtes) restaient à 800-1 100 ms — le coût du SSR initial — mais la seconde requête tombait à 45-90 ms grâce au cache Redis.\n\n### La récupération\n\nLes Core Web Vitals CrUX agrègent les données sur 28 jours glissants. L'équipe a donc dû attendre environ 3 semaines pour voir le rapport repasser au vert. Voici la chronologie :\n\n- **J+0 (mercredi)** : déploiement du fix. TTFB médian repasse sous 200 ms.\n- **J+3** : les données de lab (Lighthouse, PageSpeed Insights en mode \"simulé\") montrent des scores revenus entre 78 et 85.\n- **J+7** : Search Console commence à reclasser des URLs du bucket \"À améliorer\" vers \"Bon\".\n- **J+14** : 68 % des URLs produit sont de retour dans le bucket \"Bon\".\n- **J+22** : 94 % des URLs produit sont en \"Bon\". Le TTFB P75 sur CrUX est à 180 ms.\n\nCôté trafic organique, la chute a été mesurable mais contenue. GA4 montre une baisse de 12 % des sessions organiques sur les pages produit pendant les 10 premiers jours. Pas de déclassement brutal — Google n'a pas eu le temps de crawler massivement les pages lentes avant le fix. Si l'incident avait duré 6 semaines, le scénario aurait été bien pire, comme dans [cette migration Nuxt 2 vers Nuxt 3](/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines) où les pages en layout cassé ont mis deux mois à récupérer.\n\nLa migration aurait aussi pu causer un [problème de canonicals pointant vers un staging](/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging), si les URLs internes avaient référencé l'ancien domaine Vercel. L'équipe a vérifié — ce n'était pas le cas ici, mais c'est un piège classique.\n\n## Ce qu'on en retient\n\nMigrer un site Next.js hors de Vercel, c'est migrer hors de l'infrastructure invisible qui fait fonctionner l'ISR, l'edge caching et le stale-while-revalidate. Le code applicatif ne change pas. Le comportement en production change radicalement.\n\nTrois leçons opérationnelles :\n\n1. **Tester le TTFB, pas seulement le contenu.** Un test CI doit inclure une assertion sur le temps de réponse serveur. `expect(ttfb).toBeLessThan(500)` — c'est une ligne qui aurait tout changé.\n\n2. **Vérifier les headers de cache après chaque migration d'infra.** Un `curl -I` sur 10 URLs critiques prend 30 secondes et révèle l'absence de cache.\n\n3. **Monitorer le TTFB en continu, pas seulement au déploiement.** Les données CrUX arrivent avec 28 jours de retard. Un monitoring continu type Seogard détecte une explosion du TTFB en quelques minutes — pas trois semaines après, quand le rapport CrUX vire au rouge et que le trafic a déjà décroché.\n\nL'Edge ISR n'est pas une feature bonus. C'est le socle de performance sur lequel repose toute la stratégie SEO d'un site à 2 000 pages. Le retirer sans le remplacer, c'est passer d'un site statique ultra-rapide à un site SSR des années 2010 — et Google le voit avant l'équipe.\n```","https://seogard.io/blog/migration-vercel-vers-railway-perte-du-edge-isr-ttfb-multiplie-par-4","Migration","2026-06-01T06:01:44.888Z","2026-06-01","Récit d'une migration Next.js de Vercel vers Railway. Perte de l'Edge ISR, TTFB multiplié par 4, Core Web Vitals en chute. Diagnostic et fix complet.","\u003Ch1>Migration Vercel vers Railway : 2000 pages perdent l'Edge ISR, le TTFB explose\u003C/h1>\n\u003Cp>Jeudi 14h. L'équipe infra d'une marketplace B2B française — 2 100 pages catalogue, 380K sessions organiques par mois — finalise la migration de Vercel vers Railway. Motivation : réduire la facture d'hébergement de 40 %. Le déploiement passe. Les tests Cypress sont verts. Le DNS bascule à 16h. Vendredi soir, tout semble normal. Lundi matin, le rapport CrUX tombe. Le TTFB médian est passé de 340 ms à 1,4 seconde. Sur mobile, le LCP dépasse 4 secondes sur 73 % des pages produit.\u003C/p>\n\u003Ch2>Lundi 9h12 — Le rapport CrUX qui ne ment pas\u003C/h2>\n\u003Cp>C'est le lead SEO qui tire la sonnette. Il consulte le rapport PageSpeed Insights sur une page catégorie clé. Le score Performance mobile est tombé de 82 à 41. Le TTFB est rouge. Le LCP aussi. Le CLS est resté stable — ce n'est donc pas un problème de layout.\u003C/p>\n\u003Cp>Il ouvre Search Console. Pas encore de signal d'alerte côté indexation — trop tôt. Mais le rapport Core Web Vitals montre déjà une migration de URLs du bucket \"Bon\" vers \"À améliorer\" sur les données de terrain.\u003C/p>\n\u003Cp>À 9h35, il envoie un message au CTO : \"On a un problème de perf massif depuis la migration. Le TTFB a triplé au minimum.\"\u003C/p>\n\u003Cp>Le CTO répond vite : \"Impossible, le déploiement Railway est identique. Même image Docker, même région eu-west.\"\u003C/p>\n\u003Cp>Première hypothèse — la base de données. L'équipe vérifie les temps de réponse Postgres sur Railway. Le P95 est à 18 ms. Rien d'anormal. Les requêtes Prisma sont profilées : aucune régression.\u003C/p>\n\u003Cp>Deuxième hypothèse — le CDN. Sur Vercel, le CDN edge était intégré. Sur Railway, l'équipe a configuré Cloudflare en proxy. Mais les headers \u003Ccode>cf-cache-status\u003C/code> montrent des \u003Ccode>HIT\u003C/code> sur les assets statiques. Le problème n'est pas là.\u003C/p>\n\u003Cp>À 10h20, le développeur frontend senior tape une commande qui change tout :\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:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"TTFB: %{time_starttransfer}s\\nTotal: %{time_total}s\\nHTTP: %{http_code}\\n\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">  -H\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"User-Agent: Googlebot\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">  https://marketplace.example.com/produit/cable-hdmi-4k-2m\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat :\u003C/p>\n\u003Cpre>\u003Ccode>TTFB: 1.387s\nTotal: 1.412s\nHTTP: 200\n\u003C/code>\u003C/pre>\n\u003Cp>Même test sur le même slug, mais via le cache Wayback de la version Vercel d'il y a une semaine : TTFB de 87 ms. Le ratio est de ×16, pas ×4. Le ×4 est la médiane globale. Les pages dynamiques individuelles sont bien pires.\u003C/p>\n\u003Cp>C'est à ce moment que l'équipe comprend : le problème n'est pas un ralentissement réseau. C'est l'absence totale de cache ISR. Chaque requête déclenche un rendu SSR complet.\u003C/p>\n\u003Ch2>Le bug : quand l'Edge ISR disparaît sans laisser de trace\u003C/h2>\n\u003Cp>Pour comprendre ce qui s'est passé, il faut revenir à l'architecture en place sur Vercel.\u003C/p>\n\u003Ch3>La configuration Vercel d'origine\u003C/h3>\n\u003Cp>Le site tournait sur Next.js 14.2 avec l'App Router. Les pages produit utilisaient \u003Ccode>generateStaticParams\u003C/code> pour le pré-rendu, combiné à \u003Ccode>revalidate\u003C/code> pour l'ISR :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// app/produit/[slug]/page.tsx — version Vercel\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\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> revalidate\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 3600\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#6A737D\">// ISR : revalidation toutes les heures\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\"> generateStaticParams\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\"> products\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> prisma.product.\u003C/span>\u003Cspan style=\"color:#B392F0\">findMany\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    select: { slug: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> products.\u003C/span>\u003Cspan style=\"color:#B392F0\">map\u003C/span>\u003Cspan style=\"color:#E1E4E8\">((\u003C/span>\u003Cspan style=\"color:#FFAB70\">p\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) \u003C/span>\u003Cspan style=\"color:#F97583\">=>\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ({ slug: p.slug }));\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:#F97583\"> async\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> ProductPage\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({ \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> }\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">params\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">slug\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\"> product\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> prisma.product.\u003C/span>\u003Cspan style=\"color:#B392F0\">findUnique\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    where: { slug: params.slug },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    include: { category: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, specs: \u003C/span>\u003Cspan style=\"color:#79B8FF\">true\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\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\">product) \u003C/span>\u003Cspan style=\"color:#B392F0\">notFound\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">ProductJsonLd product\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{product} \u003C/span>\u003Cspan style=\"color:#F97583\">/>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      &#x3C;\u003C/span>\u003Cspan style=\"color:#E1E4E8\">ProductDetail product\u003C/span>\u003Cspan style=\"color:#F97583\">=\u003C/span>\u003Cspan style=\"color:#E1E4E8\">{product} \u003C/span>\u003Cspan style=\"color:#F97583\">/>\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    &#x3C;/>\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>Sur Vercel, ce code produisait un comportement précis :\u003C/p>\n\u003Col>\n\u003Cli>Au build, \u003Ccode>generateStaticParams\u003C/code> pré-rendait les 2 100 pages en HTML statique.\u003C/li>\n\u003Cli>Les pages étaient servies depuis l'edge CDN de Vercel, avec un TTFB de 40 à 90 ms.\u003C/li>\n\u003Cli>Après 3 600 secondes, une requête entrante déclenchait une revalidation en arrière-plan. L'ancienne version restait servie pendant la régénération — c'est le principe du stale-while-revalidate.\u003C/li>\n\u003Cli>Les Edge Functions de Vercel géraient cette logique nativement, sans configuration supplémentaire.\u003C/li>\n\u003C/ol>\n\u003Cp>Les headers de réponse sur Vercel ressemblaient à ceci :\u003C/p>\n\u003Cpre>\u003Ccode>x-vercel-cache: HIT\ncache-control: s-maxage=3600, stale-while-revalidate\nx-nextjs-cache: HIT\n\u003C/code>\u003C/pre>\n\u003Ch3>Ce qui se passe sur Railway\u003C/h3>\n\u003Cp>Railway exécute Next.js en mode \u003Ccode>standalone\u003C/code> via \u003Ccode>next start\u003C/code>. L'image Docker contient le serveur Node.js, point final. Il n'y a pas d'edge CDN intégré. Pas de couche de cache ISR persistante entre le serveur et le client.\u003C/p>\n\u003Cp>Le \u003Ccode>next.config.js\u003C/code> de l'équipe :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// next.config.js — version Railway\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">/** \u003C/span>\u003Cspan style=\"color:#F97583\">@type\u003C/span>\u003Cspan style=\"color:#B392F0\"> {import('next').NextConfig}\u003C/span>\u003Cspan style=\"color:#6A737D\"> */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> nextConfig\u003C/span>\u003Cspan style=\"color:#F97583\"> =\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\">'standalone'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  images: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    remotePatterns: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      { protocol: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, hostname: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'cdn.marketplace.example.com'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\"> nextConfig;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Avec \u003Ccode>output: 'standalone'\u003C/code>, Next.js génère un serveur Node minimal. Mais ce serveur n'a \u003Cstrong>aucun mécanisme de cache ISR intégré\u003C/strong>. Le répertoire \u003Ccode>.next/cache\u003C/code> qui stocke les pages ISR sur Vercel n'est pas persisté entre les redéploiements sur Railway. Pire : même à l'intérieur d'un même déploiement, le cache ISR in-memory est perdu à chaque restart du container.\u003C/p>\n\u003Cp>Résultat : chaque requête sur \u003Ccode>/produit/cable-hdmi-4k-2m\u003C/code> déclenche un rendu SSR complet. Appel Prisma. Rendu React. Sérialisation HTML. À chaque fois. Pour chaque visiteur. Pour chaque crawl Googlebot.\u003C/p>\n\u003Ch3>Ce que voit le développeur vs ce que voit Googlebot\u003C/h3>\n\u003Cp>Le développeur teste dans Chrome. La page se charge en 600 ms environ — le navigateur est à Paris, le serveur Railway aussi, et le navigateur a du cache local sur les fonts et le CSS.\u003C/p>\n\u003Cp>Googlebot n'a pas ce luxe. Il crawle depuis des IPs américaines (principalement). Il n'a pas de cache local. Il exécute chaque requête à froid. Et il reçoit un TTFB de 1,2 à 1,8 seconde selon la complexité de la page.\u003C/p>\n\u003Cp>Pour vérifier, l'équipe utilise Chrome DevTools en throttling \"Slow 3G\" et en mode Incognito — ça se rapproche de l'expérience réelle, mais pas complètement. Le vrai diagnostic vient de l'outil d'inspection d'URL de Search Console, qui affiche le HTML rendu tel que Googlebot le reçoit, avec le temps de chargement.\u003C/p>\n\u003Cp>Sur 2 100 pages produit, Screaming Frog en mode rendu JavaScript révèle des temps de réponse moyens de 1 340 ms, contre 280 ms relevés une semaine avant la migration sur un crawl archivé.\u003C/p>\n\u003Ch3>Pourquoi les tests CI n'ont rien détecté\u003C/h3>\n\u003Cp>L'équipe avait des tests Cypress end-to-end. Mais ces tests vérifient le contenu rendu, pas la performance de rendu. Un \u003Ccode>cy.get('[data-testid=\"product-title\"]').should('exist')\u003C/code> passe en 1,4 seconde comme en 90 ms.\u003C/p>\n\u003Cp>Les tests Lighthouse CI étaient configurés pour tourner sur un environnement de staging Railway. Le staging affichait des scores corrects — parce qu'il n'avait que 50 produits en base, pas 2 100. Le SSR de 50 pages sans cache ne produit pas la même charge qu'un SSR de 2 100 pages sous trafic réel.\u003C/p>\n\u003Cp>Aucun test ne comparait le TTFB avant/après migration. Aucun test ne vérifiait la présence des headers de cache ISR. C'est un angle mort classique des pipelines CI pour le SEO technique — un problème détaillé dans \u003Ca href=\"/blog/how-to-stress-test-a-staging-environment-to-surface-risks-pre-launch-ask-an-seo-via-sejournal-helenpollitt1\">cet article sur le stress testing des environnements de staging\u003C/a>.\u003C/p>\n\u003Cp>Le parallèle avec d'autres migrations est frappant. Comme lors d'un \u003Ca href=\"/blog/migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees\">passage de Cloudflare vers Bunny CDN\u003C/a>, c'est la couche d'infrastructure \"invisible\" qui casse silencieusement — pas le code applicatif.\u003C/p>\n\u003Ch2>Le fix : reconstruire une couche de cache ISR sans Vercel\u003C/h2>\n\u003Cp>L'équipe a exploré trois options. La première — revenir sur Vercel — a été écartée pour des raisons budgétaires. La deuxième — implémenter un cache ISR custom avec Redis — a été retenue. La troisième — passer à un export statique complet — était incompatible avec les besoins de personnalisation en temps réel.\u003C/p>\n\u003Ch3>Le patch : cache ISR via Redis sur Railway\u003C/h3>\n\u003Cp>Railway supporte nativement les instances Redis. L'équipe a déployé un plugin Redis et configuré un \u003Ca href=\"https://nextjs.org/docs/app/building-your-application/deploying#caching-and-isr\">cache handler custom pour Next.js\u003C/a>, disponible depuis Next.js 14.1.\u003C/p>\n\u003Cp>Étape 1 — Le cache handler custom :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// lib/cache-handler.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { CacheHandler } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/dist/server/lib/incremental-cache'\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\"> Redis \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'ioredis'\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\"> redis\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> new\u003C/span>\u003Cspan style=\"color:#B392F0\"> Redis\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(process.env.\u003C/span>\u003Cspan style=\"color:#79B8FF\">REDIS_URL\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> CACHE_PREFIX\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next-isr:'\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:#F97583\"> class\u003C/span>\u003Cspan style=\"color:#B392F0\"> RedisCacheHandler\u003C/span>\u003Cspan style=\"color:#F97583\"> extends\u003C/span>\u003Cspan style=\"color:#B392F0\"> CacheHandler\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  constructor\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">options\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> any\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    super\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(options);\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\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">key\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\"> data\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redis.\u003C/span>\u003Cspan style=\"color:#B392F0\">get\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">CACHE_PREFIX\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">key\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">!\u003C/span>\u003Cspan style=\"color:#E1E4E8\">data) \u003C/span>\u003Cspan style=\"color:#F97583\">return\u003C/span>\u003Cspan style=\"color:#79B8FF\"> null\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\"> parsed\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">parse\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(data);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      value: parsed.value,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      lastModified: parsed.lastModified,\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\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">key\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">data\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> any\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#FFAB70\">ctx\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { \u003C/span>\u003Cspan style=\"color:#FFAB70\">revalidate\u003C/span>\u003Cspan style=\"color:#F97583\">?:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> number\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#79B8FF\"> false\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\"> payload\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#79B8FF\"> JSON\u003C/span>\u003Cspan style=\"color:#E1E4E8\">.\u003C/span>\u003Cspan style=\"color:#B392F0\">stringify\u003C/span>\u003Cspan style=\"color:#E1E4E8\">({\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      value: data,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      lastModified: Date.\u003C/span>\u003Cspan style=\"color:#B392F0\">now\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    });\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (ctx.revalidate \u003C/span>\u003Cspan style=\"color:#F97583\">&#x26;&#x26;\u003C/span>\u003Cspan style=\"color:#F97583\"> typeof\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> ctx.revalidate \u003C/span>\u003Cspan style=\"color:#F97583\">===\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'number'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">      // TTL = revalidate + 60s de grâce pour le stale-while-revalidate\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redis.\u003C/span>\u003Cspan style=\"color:#B392F0\">setex\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">CACHE_PREFIX\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">key\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, ctx.revalidate \u003C/span>\u003Cspan style=\"color:#F97583\">+\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 60\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, payload);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    } \u003C/span>\u003Cspan style=\"color:#F97583\">else\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redis.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">CACHE_PREFIX\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">key\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, payload);\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\">  async\u003C/span>\u003Cspan style=\"color:#B392F0\"> revalidateTag\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">tag\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#79B8FF\"> string\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\"> keys\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#F97583\"> await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redis.\u003C/span>\u003Cspan style=\"color:#B392F0\">keys\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">`${\u003C/span>\u003Cspan style=\"color:#79B8FF\">CACHE_PREFIX\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}*`\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    // Implémentation simplifiée — en prod, utiliser un index par tag\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (\u003C/span>\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> key\u003C/span>\u003Cspan style=\"color:#F97583\"> of\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> keys) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">      await\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> redis.\u003C/span>\u003Cspan style=\"color:#B392F0\">del\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(key);\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>Étape 2 — La configuration Next.js mise à 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\">// next.config.js — version Railway avec cache handler\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">/** \u003C/span>\u003Cspan style=\"color:#F97583\">@type\u003C/span>\u003Cspan style=\"color:#B392F0\"> {import('next').NextConfig}\u003C/span>\u003Cspan style=\"color:#6A737D\"> */\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> nextConfig\u003C/span>\u003Cspan style=\"color:#F97583\"> =\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\">'standalone'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  cacheHandler: require.\u003C/span>\u003Cspan style=\"color:#B392F0\">resolve\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'./lib/cache-handler.ts'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">),\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  cacheMaxMemorySize: \u003C/span>\u003Cspan style=\"color:#79B8FF\">0\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#6A737D\">// Désactiver le cache in-memory, tout passe par Redis\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  images: {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    remotePatterns: [\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">      { protocol: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'https'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, hostname: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'cdn.marketplace.example.com'\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ],\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  },\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">};\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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\"> nextConfig;\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Étape 3 — Ajout de headers cache-control via le middleware Cloudflare pour que le CDN en amont serve du stale-while-revalidate :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">// middleware.ts\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { NextResponse } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/server'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C/span>\u003Cspan style=\"color:#F97583\"> type\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> { NextRequest } \u003C/span>\u003Cspan style=\"color:#F97583\">from\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> 'next/server'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> function\u003C/span>\u003Cspan style=\"color:#B392F0\"> middleware\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#FFAB70\">request\u003C/span>\u003Cspan style=\"color:#F97583\">:\u003C/span>\u003Cspan style=\"color:#B392F0\"> NextRequest\u003C/span>\u003Cspan style=\"color:#E1E4E8\">) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> response\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> NextResponse.\u003C/span>\u003Cspan style=\"color:#B392F0\">next\u003C/span>\u003Cspan style=\"color:#E1E4E8\">();\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  if\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> (request.nextUrl.pathname.\u003C/span>\u003Cspan style=\"color:#B392F0\">startsWith\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/produit/'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">)) {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      'Cache-Control'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">,\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">      's-maxage=3600, stale-while-revalidate=86400'\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    );\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    response.headers.\u003C/span>\u003Cspan style=\"color:#B392F0\">set\u003C/span>\u003Cspan style=\"color:#E1E4E8\">(\u003C/span>\u003Cspan style=\"color:#9ECBFF\">'CDN-Cache-Control'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">, \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'s-maxage=3600'\u003C/span>\u003Cspan style=\"color:#E1E4E8\">);\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  }\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">  return\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> response;\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">export\u003C/span>\u003Cspan style=\"color:#F97583\"> const\u003C/span>\u003Cspan style=\"color:#79B8FF\"> config\u003C/span>\u003Cspan style=\"color:#F97583\"> =\u003C/span>\u003Cspan style=\"color:#E1E4E8\"> {\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">  matcher: \u003C/span>\u003Cspan style=\"color:#9ECBFF\">'/produit/:slug*'\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\u003Ch3>Le redéploiement\u003C/h3>\n\u003Cp>Le déploiement a eu lieu mercredi à 11h. L'équipe a ensuite déclenché un crawl ciblé via \u003Ccode>curl\u003C/code> sur les 200 pages les plus trafiquées pour chauffer le cache Redis :\u003C/p>\n\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># warm-cache.sh — pré-chauffage des pages critiques\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">cat\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> top-200-slugs.txt\u003C/span>\u003Cspan style=\"color:#F97583\"> |\u003C/span>\u003Cspan style=\"color:#F97583\"> while\u003C/span>\u003Cspan style=\"color:#79B8FF\"> read\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> slug\u003C/span>\u003Cspan style=\"color:#E1E4E8\">; \u003C/span>\u003Cspan style=\"color:#F97583\">do\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  curl\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -s\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -o\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> /dev/null\u003C/span>\u003Cspan style=\"color:#79B8FF\"> -w\u003C/span>\u003Cspan style=\"color:#9ECBFF\"> \"%{url_effective} → TTFB: %{time_starttransfer}s\\n\"\u003C/span>\u003Cspan style=\"color:#79B8FF\"> \\\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"https://marketplace.example.com/produit/${\u003C/span>\u003Cspan style=\"color:#E1E4E8\">slug\u003C/span>\u003Cspan style=\"color:#9ECBFF\">}\"\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#B392F0\">  sleep\u003C/span>\u003Cspan style=\"color:#79B8FF\"> 0.2\u003C/span>\u003C/span>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">done\u003C/span>\u003C/span>\u003C/code>\u003C/pre>\n\u003Cp>Résultat immédiat après warm-up : TTFB médian retombé à 120 ms sur les pages cachées. Les pages non-cachées (premières requêtes) restaient à 800-1 100 ms — le coût du SSR initial — mais la seconde requête tombait à 45-90 ms grâce au cache Redis.\u003C/p>\n\u003Ch3>La récupération\u003C/h3>\n\u003Cp>Les Core Web Vitals CrUX agrègent les données sur 28 jours glissants. L'équipe a donc dû attendre environ 3 semaines pour voir le rapport repasser au vert. Voici la chronologie :\u003C/p>\n\u003Cul>\n\u003Cli>\u003Cstrong>J+0 (mercredi)\u003C/strong> : déploiement du fix. TTFB médian repasse sous 200 ms.\u003C/li>\n\u003Cli>\u003Cstrong>J+3\u003C/strong> : les données de lab (Lighthouse, PageSpeed Insights en mode \"simulé\") montrent des scores revenus entre 78 et 85.\u003C/li>\n\u003Cli>\u003Cstrong>J+7\u003C/strong> : Search Console commence à reclasser des URLs du bucket \"À améliorer\" vers \"Bon\".\u003C/li>\n\u003Cli>\u003Cstrong>J+14\u003C/strong> : 68 % des URLs produit sont de retour dans le bucket \"Bon\".\u003C/li>\n\u003Cli>\u003Cstrong>J+22\u003C/strong> : 94 % des URLs produit sont en \"Bon\". Le TTFB P75 sur CrUX est à 180 ms.\u003C/li>\n\u003C/ul>\n\u003Cp>Côté trafic organique, la chute a été mesurable mais contenue. GA4 montre une baisse de 12 % des sessions organiques sur les pages produit pendant les 10 premiers jours. Pas de déclassement brutal — Google n'a pas eu le temps de crawler massivement les pages lentes avant le fix. Si l'incident avait duré 6 semaines, le scénario aurait été bien pire, comme dans \u003Ca href=\"/blog/migration-nuxt-2-vers-nuxt-3-200-pages-en-fallback-layout-pendant-6-semaines\">cette migration Nuxt 2 vers Nuxt 3\u003C/a> où les pages en layout cassé ont mis deux mois à récupérer.\u003C/p>\n\u003Cp>La migration aurait aussi pu causer un \u003Ca href=\"/blog/migration-prestashop-vers-bigcommerce-canonicals-pointent-encore-vers-le-staging\">problème de canonicals pointant vers un staging\u003C/a>, si les URLs internes avaient référencé l'ancien domaine Vercel. L'équipe a vérifié — ce n'était pas le cas ici, mais c'est un piège classique.\u003C/p>\n\u003Ch2>Ce qu'on en retient\u003C/h2>\n\u003Cp>Migrer un site Next.js hors de Vercel, c'est migrer hors de l'infrastructure invisible qui fait fonctionner l'ISR, l'edge caching et le stale-while-revalidate. Le code applicatif ne change pas. Le comportement en production change radicalement.\u003C/p>\n\u003Cp>Trois leçons opérationnelles :\u003C/p>\n\u003Col>\n\u003Cli>\n\u003Cp>\u003Cstrong>Tester le TTFB, pas seulement le contenu.\u003C/strong> Un test CI doit inclure une assertion sur le temps de réponse serveur. \u003Ccode>expect(ttfb).toBeLessThan(500)\u003C/code> — c'est une ligne qui aurait tout changé.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Vérifier les headers de cache après chaque migration d'infra.\u003C/strong> Un \u003Ccode>curl -I\u003C/code> sur 10 URLs critiques prend 30 secondes et révèle l'absence de cache.\u003C/p>\n\u003C/li>\n\u003Cli>\n\u003Cp>\u003Cstrong>Monitorer le TTFB en continu, pas seulement au déploiement.\u003C/strong> Les données CrUX arrivent avec 28 jours de retard. Un monitoring continu type Seogard détecte une explosion du TTFB en quelques minutes — pas trois semaines après, quand le rapport CrUX vire au rouge et que le trafic a déjà décroché.\u003C/p>\n\u003C/li>\n\u003C/ol>\n\u003Cp>L'Edge ISR n'est pas une feature bonus. C'est le socle de performance sur lequel repose toute la stratégie SEO d'un site à 2 000 pages. Le retirer sans le remplacer, c'est passer d'un site statique ultra-rapide à un site SSR des années 2010 — et Google le voit avant l'équipe.\u003C/p>\n\u003Cpre>\u003Ccode>\u003C/code>\u003C/pre>",null,12,[18,19,20,21,22],"vercel","railway","edge","isr","ttfb","Migration Vercel → Railway : TTFB ×4, 2000 pages sans Edge ISR","Mon Jun 01 2026 06:01:44 GMT+0000 (Coordinated Universal Time)",[26,41,55],{"_id":27,"slug":28,"__v":6,"author":7,"canonical":29,"category":10,"createdAt":30,"date":31,"description":32,"image":15,"imageAlt":15,"readingTime":16,"tags":33,"title":39,"updatedAt":40},"6a1bceceaa6b273b0ce39021","migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees","https://seogard.io/blog/migration-cloudflare-vers-bunny-cdn-regles-redirect-https-oubliees","2026-05-31T06:01:50.903Z","2026-05-31","Récit d'une migration CDN où les Page Rules Cloudflare n'ont pas été portées vers Bunny. 302 silencieux, jus SEO perdu, et fix complet.",[34,35,36,37,38],"cloudflare","bunny cdn","redirect 301","migration cdn","seo technique","Migration Cloudflare → Bunny CDN : 302 au lieu de 301 pendant 2 mois","Sun May 31 2026 06:01:50 GMT+0000 (Coordinated Universal Time)",{"_id":42,"slug":43,"__v":6,"author":7,"canonical":44,"category":10,"createdAt":45,"date":46,"description":47,"image":15,"imageAlt":15,"readingTime":16,"tags":48,"title":53,"updatedAt":54},"6a1b09e9aa6b273b0c40f580","passage-http-vers-https-imparfait-mixed-content-sur-les-images-cdn","https://seogard.io/blog/passage-http-vers-https-imparfait-mixed-content-sur-les-images-cdn","2026-05-30T16:01:45.919Z","2026-05-30","Migration HTTPS réussie, mais les images CDN restent en HTTP. Récit d'un mixed content invisible qui a coûté 34% de clics en 5 semaines.",[49,50,51,52],"https","mixed content","cdn","security","Mixed content CDN : HTTPS cassé par des images HTTP","Sat May 30 2026 16:01:45 GMT+0000 (Coordinated Universal Time)",{"_id":56,"slug":57,"__v":6,"author":7,"canonical":58,"category":10,"createdAt":59,"date":60,"description":61,"image":15,"imageAlt":15,"readingTime":16,"tags":62,"title":67,"updatedAt":68},"6a192bc6aa6b273b0cb63757","migration-webflow-vers-framer-301-hub-and-spoke-perdues-pagerank-dilue","https://seogard.io/blog/migration-webflow-vers-framer-301-hub-and-spoke-perdues-pagerank-dilue","2026-05-29T06:01:42.654Z","2026-05-29","Récit d'une migration Webflow vers Framer où 301 redirects hub-and-spoke disparaissent. Diagnostic, impact sur 90 jours, et fix complet.",[63,64,65,66],"webflow","framer","migration","redirects","Migration Webflow → Framer : 301 perdues, PageRank dilué","Fri May 29 2026 06:01:42 GMT+0000 (Coordinated Universal Time)"]