Article d’expertise · Migration React vers Next.js
Le risque d’une migration React ne se trouve pas dans les composants, mais aux frontières : URLs, sessions, appels API, cache, analytics et rythme de livraison. Pour une page publique, il faut aussi examiner ce que le navigateur et les robots reçoivent avant d’exécuter le JavaScript.
La bonne question n’est pas « combien de temps pour tout réécrire ? ». Il faut d’abord choisir une page publique où Next.js peut livrer un HTML utile, des métadonnées fiables et potentiellement moins de JavaScript. Le trafic de cette URL doit aussi pouvoir être redirigé vers la SPA si la version migrée se dégrade en production.
Le point de départ
Une SPA en production et une roadmap qui continue
Le point de départ est courant : une plateforme B2B combine un catalogue public et un espace connecté. Le frontend React, construit avec Vite et React Router, consomme une API Node.js stable. Les services sont déployés dans Docker et la roadmap continue pendant la migration.
Le besoin n’est pas de remplacer Vite par principe. La réponse HTML des pages publiques contient surtout une coquille vide. Le titre, le contenu, les liens et parfois la balise canonique n’apparaissent qu’après le téléchargement de l’application et plusieurs appels API. Certains chargements se font en cascade, tandis que le routage, la gestion de session et l’état global restent fortement couplés à l’implémentation de la SPA. Une branche de réécriture maintenue plusieurs mois ne ferait que reporter le risque à la bascule.
Le mandat
Migrer sans redesign, sans réécrire l’API et sans interrompre la roadmap.
- Conserver les URLs et les sessions existantes.
- Basculer et observer une route à la fois.
- Livrer le contenu public et ses métadonnées dans la réponse serveur.
- Conserver dans le reverse proxy une règle de rollback indépendante du build Next.js.
Pourquoi Next.js
Rendre le contenu indexable sans déléguer tout le rendu au JavaScript côté client
Google sait exécuter JavaScript, mais le crawl, le rendu et l’indexation restent trois étapes distinctes. Une SPA qui renvoie un conteneur vide demande au robot de télécharger l’application, lancer React puis attendre les données avant de voir le contenu. Tous les robots ne disposent pas de cette capacité.
Dans l’App Router, les pages et les layouts sont des React Server Components par défaut. Ils peuvent charger les données près de leur source, produire le contenu sur le serveur et participer au pré-rendu HTML. Leur code n’est pas ajouté au bundle navigateur. Les filtres, boutons et autres interactions restent des Client Components placés au plus près du besoin.
| Point observé | SPA React | Next.js avec RSC |
|---|---|---|
| Contenu principal | Ajouté après le chargement du JavaScript et les appels API. | Présent dans le HTML pré-rendu à partir des Server Components. |
| Chargement des données | Déclenché dans le navigateur, souvent après le premier rendu. | Effectué côté serveur avant de produire la partie concernée de la page. |
| JavaScript côté navigateur | Prend en charge le routage, le chargement des données et le rendu de la page. | Provient du runtime React/Next et des Client Components nécessaires ; son volume doit être mesuré. |
| Métadonnées | Gérées globalement ou modifiées pendant l’exécution client. | Produites par page avec la Metadata API. |
| Exploration | Dépend davantage de la capacité du robot à exécuter le JavaScript. | Expose le contenu et les liens internes dans la réponse HTML. |
L’avantage SEO ne vient pas d’un signal de classement réservé à Next.js. Il vient de la maîtrise de la réponse : un statut HTTP cohérent, un titre, une description, une URL canonique, le contenu principal et les liens internes peuvent être disponibles sans attendre l’exécution client.
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { cache } from 'react'
import { readResource } from '@/lib/resources'
import { ShareButton } from './share-button'
type Props = { params: Promise<{ slug: string }> }
const getResource = cache(readResource)
export async function generateMetadata({
params,
}: Props): Promise<Metadata> {
const resource = await getResource((await params).slug)
if (!resource) return {}
return {
title: resource.seoTitle,
description: resource.excerpt,
alternates: {
canonical: `/ressources/${resource.slug}`,
},
openGraph: {
title: resource.seoTitle,
description: resource.excerpt,
},
}
}
export default async function Page({ params }: Props) {
const resource = await getResource((await params).slug)
if (!resource) notFound()
return (
<article>
<h1>{resource.title}</h1>
<div>{resource.content}</div>
<ShareButton />
</article>
)
}La Metadata API couvre les balises de la page et le partage social. Les conventions sitemap.ts, robots.ts et les images Open Graph évitent de disperser cette configuration. Les données structurées JSON-LD restent explicites et doivent décrire exactement le contenu visible.
Les critères SEO d’une page migrée
Vérifier la réponse produite, pas seulement le DOM après hydratation.
- Le HTML contient le H1, le contenu utile et les liens internes avec JavaScript désactivé.
- Le statut HTTP, l’URL canonique et les redirections correspondent au cas métier.
- Le titre, la description et les cartes sociales sont propres à la page.
- Le JSON-LD décrit le contenu visible et passe les outils de validation.
- Le sitemap référence l’URL finale, sans dupliquer l’ancien parcours.
L’investigation
Choisir un parcours pilote, pas le composant le plus visible
L’audit commence par les routes. Pour chacune, nous relevons l’audience, l’interactivité, les dépendances navigateur, la valeur SEO et le coût métier d’une régression. Nous vérifions ensuite le HTML reçu sans JavaScript, les statuts, les métadonnées, les liens internes, les données structurées, les cookies, les deep links, les variables VITE_*, analytics, service worker et styles globaux.
| Parcours | Profil | Couplage | Décision |
|---|---|---|---|
| /ressources/[slug] | Public, contenu éditorial, enjeu SEO | Faible | Pilote Next.js + RSC |
| /catalogue | Public, filtres interactifs | Moyen | Deuxième vague |
| /app/dashboard | Session, données dynamiques | Fort | Conservé dans la SPA |
Un état initial complète le diagnostic : taux d’erreur, LCP, INP, CLS, TTFB, volume de JavaScript et appels réseau. Pour les pages publiques, nous ajoutons les impressions, les URLs découvertes, les erreurs d’exploration et le HTML observé dans Search Console. Ces mesures servent à détecter une régression, pas à promettre un gain avant la première livraison.
Le choix structurant
Faire cohabiter les deux frontends derrière les mêmes URLs
Next.js prend en charge les routes migrées et la SPA continue de répondre au reste. L’API Node.js reste la source de vérité métier. Le fallback Next.js délègue les routes qu’il ne sert pas encore. Pour restaurer une route déjà migrée sans reconstruire l’application, le reverse proxy doit pouvoir rediriger explicitement son trafic vers la SPA.
Next.js
Routes migrées · HTML et RSC
SPA React / Vite
Routes restantes · rendu client
import type { NextConfig } from 'next'
const legacyOrigin = process.env.LEGACY_FRONTEND_ORIGIN
if (!legacyOrigin) {
throw new Error('LEGACY_FRONTEND_ORIGIN is required')
}
const nextConfig = {
output: 'standalone',
async rewrites() {
return {
fallback: [{
source: '/:path*',
destination: legacyOrigin + '/:path*',
}],
}
},
} satisfies NextConfig
export default nextConfigLe fallback est évalué après les routes Next.js ; il facilite donc l’adoption progressive, mais ne suffit pas à restaurer une route déjà migrée. La règle de rollback vit dans l’ingress ou le reverse proxy, puis elle est testée avec les query strings, les assets Vite, les deep links et les pages d’erreur.
La réalisation
Cinq étapes pour livrer sans tunnel de migration
- 1
Écrire les invariants
URLs, cookies, contrats API, événements analytics et redirections deviennent la Definition of Done commune aux deux applications. - 2
Déployer le shell
Le service Next.js démarre dans Docker, se connecte à l’API, expose un health check et produit des logs avant de servir du trafic utilisateur. - 3
Migrer une tranche
Une page publique est traitée de bout en bout : données, HTML initial, métadonnées, JSON-LD, liens internes, erreurs, analytics, tests et rollback. - 4
Limiter le JavaScript client aux interactions
Les pages et layouts restent côté serveur par défaut. La directive « use client » est réservée aux composants de recherche, aux filtres et aux boutons qui ont besoin d’état ou des API du navigateur. - 5
Basculer et observer
La route est activée avec son tableau de bord, une règle de proxy testée et la personne habilitée à déclencher le rollback. En cas de dégradation, le trafic de cette route est redirigé vers la SPA.
Les React Server Components ne signifient pas que toutes les pages deviennent statiques. Une ressource éditoriale peut être pré-rendue puis revalidée, tandis que des données à jour peuvent être chargées et rendues à la demande. Dans les deux cas, la réponse initiale doit présenter un statut, des métadonnées et un contenu principal cohérents.
Le mode output: 'standalone' produit le dossier de déploiement utilisé par l’image Docker. Les dossiers public et .next/static doivent aussi être copiés. Sur plusieurs replicas, la stratégie de cache et de revalidation doit être explicitement coordonnée.
Équipe et conclusion
La migration se termine quand le legacy disparaît
La première tranche est réalisée en binôme avec le consultant. Les décisions structurantes sont consignées dans de courtes ADR. En review, l’équipe pose quatre questions : où ce code s’exécute-t-il, quelles données sont mises en cache, quel contenu HTML et quel JavaScript sont envoyés au navigateur, et comment revient-on au parcours legacy ?
Après activation, nous inspectons la réponse dans Search Console, puis comparons taux d’erreur, Core Web Vitals de terrain, latence, JavaScript transféré et volume des appels API. Les impressions et l’indexation se lisent sur une période plus longue. Un score Lighthouse isolé ne valide pas la migration. Le rollback est testé avant la suppression de la route historique.
Lorsque plus aucun trafic utile ne passe par la SPA, nous retirons les règles de fallback et les assets Vite, désenregistrons le service worker et vidons ses caches, puis supprimons ses dépendances et son image Docker. La cohabitation est une étape, pas une architecture cible.
Next.js ne garantit pas une position dans les résultats. Il permet de reprendre la maîtrise du HTML, des métadonnées et du JavaScript envoyé. La migration progressive vérifie ce bénéfice route par route, sans mettre toute la production en jeu.
Sources et documentation de référence
- Migrating from Vite. Documentation Next.js. Le chemin officiel pour adopter Next.js progressivement depuis une SPA existante.
- Rewrites. Documentation Next.js. Les règles de routage utilisées pour déléguer les routes non migrées.
- Server and Client Components. Documentation Next.js. Le placement de la frontière serveur/client dans l’App Router.
- Metadata and OG images. Documentation Next.js. Les API de métadonnées statiques et dynamiques disponibles dans les Server Components.
- Metadata Files. Documentation Next.js. Les conventions pour robots.txt, sitemap.xml, icônes et images de partage.
- JavaScript SEO basics. Google Search Central. Les étapes de crawl, de rendu et d’indexation, ainsi que l’intérêt du rendu serveur pour les utilisateurs et les robots.
- How to self-host your Next.js application. Documentation Next.js. Reverse proxy, cache et contraintes d’un déploiement conteneurisé.
- Containerize a Next.js application. Documentation Docker. Un exemple maintenu de build multi-stage et d’exécution en conteneur.