< BACK Vieille carte en papier usé de l'Europe sur un bureau en bois avec une tasse d'espresso, lumière éditoriale grise, grain de film 35 mm

Next.js i18n en 2026 : routage, hreflang et SEO

En 2023, j'ai lancé un site e-commerce multilingue pour un client basé à Berlin. Douze langues, App Router, tout semblait propre. Trois mois plus tard, leurs pages de produits en allemand se classaient dans les SERP britanniques et leurs pages en anglais étaient servies aux utilisateurs à Munich. Les balises hreflang étaient techniquement présentes. Elles étaient juste mauvaises d'une manière qui m'a pris un après-midi embarrassant à démêler. Cette expérience m'a appris plus sur Next.js i18n que n'importe quelle page de documentation.

Ce n'est pas un post « voici les docs officielles résumées ». C'est ce que j'ai appris en construisant des sites multilingues chez Seahawk Media, ce qui pose problème à répétition, et les décisions spécifiques qui affectent vraiment les classements en 2026.

---

L'App Router a Changé la Donne (Et pas que en bien)

Le Pages Router avait un support i18n natif. Vous laissiez tomber un bloc dans next.config.js et Next.js gérait la détection de locale, le routage par sous-chemin et le changement de locale du composant Link presque automatiquement. Ce n'était pas parfait, mais c'était directif, ce qui signifiait moins de pièges.next.config.js and Next.js handled locale detection, sub-path routing, and Link component locale switching almost automatically. It wasn't perfect, but it was opinionated, which meant fewer footguns.

L'App Router a supprimé le routage i18n natif entièrement. Pas de clé i18n dans next.config.js. Pas de détection automatique de locale. Vous êtes maintenant responsable de toute la logique de routage, et c'est là que la plupart des équipes font discrètement du désordre.i18n key in next.config.js. No automatic locale detection. You are now responsible for the full routing logic, and that responsibility is where most teams quietly make a mess.

L'approche recommandée en 2026 est un segment dynamique [locale] à la racine de votre répertoire app/. Quelque chose comme app/[locale]/page.tsx. Associez cela à next-intl ou i18next via react-i18next, un fichier middleware qui lit les en-têtes Accept-Language et redirige en conséquence, et vous avez une base fonctionnelle.[locale] dynamic segment at the root of your app/ directory. Something like app/[locale]/page.tsx . Pair that with next-intl or i18next via react-i18next, a middleware file that reads Accept-Language headers and redirects accordingly, and you have a working foundation.

Pourquoi le Middleware Est Mal Compris

Le fichier middleware fait deux tâches distinctes que les gens confondent. Premièrement, la détection de locale : lire l'en-tête Accept-Language (ou un cookie, ou un préfixe d'URL) et décider quelle locale servir. Deuxièmement, rediriger ou réécrire l'URL pour que l'utilisateur se retrouve à /de/produkte plutôt qu'à /produkte avec un état de locale caché.Accept-Language header (or a cookie, or a URL prefix) and deciding which locale to serve. Second, redirecting or rewriting the URL so the user ends up at /de/produkte rather than /produkte with some hidden locale state.

Les deux doivent se produire avant que la page ne s'affiche. Ça semble évident. Mais j'ai vu des équipes exécuter la détection de locale à l'intérieur d'un fournisseur de contexte React, ce qui signifie que le HTML initial servi à Googlebot n'a aucun signal de locale du tout. Le bot voit une page, l'indexe sous l'URL racine, et soudain vous avez un désordre de canoniques.before the page renders. That sounds obvious. But I've seen teams run locale detection inside a React context provider, which means the initial HTML served to Googlebot has no locale signal at all. The bot sees a page, it indexes it under the root URL, and suddenly you have a canonical mess.

Gardez la résolution de locale dans le middleware. Toujours.

---

Sous-chemin vs Sous-domaine : Choisissez Un et Engagez-Vous

Le débat éternel. /en/about vs en.yoursite.com/about. Pour la plupart des projets je recommande le routage par sous-chemin, et voici pourquoi : il concentre votre autorité de domaine. Chaque lien pointant vers votre .com bénéficie à toutes les locales. Avec les sous-domaines vous exécutez effectivement des sites séparés aux yeux de Google, ce qui signifie que vous devez construire l'autorité pour chacun indépendamment./en/about vs en.yoursite.com/about . For most projects I recommend sub-path routing, and here's why: it concentrates your domain authority. Every link pointing at your .com benefits all locales. With sub-domains you're effectively running separate sites in Google's eyes, which means you need to build authority for each one independently.

Il y a une exception. Si vous êtes sur un marché où le domaine de premier niveau local compte énormément pour la confiance (.de en Allemagne, .fr en France), un domaine de premier niveau avec code pays surpasse à la fois les sous-chemins et les sous-domaines pour la crédibilité locale perçue. Ce n'est pas une affirmation SEO, c'est une affirmation de conversion. J'ai vu des tests A/B où .de vs yourbrand.com/de ont déplacé les métriques de confiance de manière significative même quand les classements étaient identiques..de in Germany, .fr in France), a country-code TLD beats both sub-paths and sub-domains for perceived local credibility. That's not an SEO claim, it's a conversion claim. I've seen A/B tests where .de vs yourbrand.com/de moved trust metrics meaningfully even when rankings were identical.

Mais vous ne pouvez pas facilement configurer un ccTLD dans une seule application Next.js. Donc pour 99% des projets : des sous-chemins, une application, un domaine.

---

hreflang : L'étiquette qui vous ment

C'est là que je vois le plus de dégâts. hreflang n'est pas difficile conceptuellement. Elle dit aux moteurs de recherche : « cette page a des équivalents dans d'autres langues, les voici ». En pratique, l'implémentation a trois modes de défaillance spécifiques que j'observe constamment.

Mode de défaillance 1 : Les étiquettes ne sont pas réciproques

Chaque page dans votre ensemble hreflang doit référencer toutes les autres pages, y compris elle-même. Si /en/about a une hreflang pointant vers /de/about mais que /de/about n'a pas une hreflang pointant en retour vers /en/about, Google ignore l'ensemble. Pas de pénalité. Ignore. Comme si les étiquettes n'existaient pas./en/about has an hreflang pointing to /de/about but /de/about does not have an hreflang pointing back to /en/about, Google ignores the whole set. Not penalises. Ignores. As if the tags don't exist.

Seahawk avait un projet fintech l'année dernière où 40% de leurs étiquettes hreflang n'étaient pas réciproques parce que deux développeurs les avaient implémentées indépendamment dans différents composants sans coordination. La correction a pris une heure. La récupération des classements a pris deux mois.

Mode de défaillance 2 : L'étiquette x-default manque ou est mal placée

x-default est pour les pages qui ne ciblent pas une langue ou une région spécifique. Généralement votre racine / ou une page de sélecteur de langue. Certaines équipes mettent x-default sur leur page anglaise, ce qui est correct si l'anglais est vraiment votre fallback pour tous les locales non reconnus. Mais si vous servez une page différente aux utilisateurs dont vous ne supportez pas la langue, x-default devrait pointer vers celle-ci. is for pages that don't target a specific language or region. Typically your root / or a language-selector page. Some teams put x-default on their English page, which is fine if English is genuinely your fallback for all unmatched locales. But if you serve a different page to users whose language you don't support, x-default should point there.

J'ai vu x-default omis entièrement. Google ne lance pas d'erreur. Il prend simplement sa propre décision sur la version à afficher pour les requêtes non reconnues, et cette décision est souvent mauvaise.x-default omitted entirely. Google doesn't throw an error. It just makes its own decision about which version to show for unmatched queries, and that decision is often wrong.

Mode de défaillance 3 : Barres obliques finales et conflits canoniques

Votre balise hreflang indique https://yoursite.com/de/ueber-uns . Votre balise canonique sur la même page indique https://yoursite.com/de/ueber-uns/ (notez la barre oblique finale). Google traite ces deux URLs comme différentes. Votre balise canonique et votre hreflang pointent maintenant vers des choses différentes, et vous avez créé un conflit de signal qui supprime silencieusement les classements sans aucune erreur visible dans Search Console.https://yoursite.com/de/ueber-uns . Your canonical tag on the same page says https://yoursite.com/de/ueber-uns/ (note the trailing slash). Google treats those as different URLs. Your canonical and hreflang are now pointing at different things, and you've created a signal conflict that quietly suppresses rankings without any visible error in Search Console.

Définissez trailingSlash dans next.config.js et soyez cohérent partout. Vérifiez-le. Vérifiez-le réellement en envoyant des requêtes curl à vos propres URLs et en lisant le résultat.trailingSlash in next.config.js and be consistent everywhere. Check it. Actually check it by curling your own URLs and reading the output.

La documentation Google Search Central sur hreflang vaut vraiment la peine d'être lue en intégralité. C'est l'un de ces rares documents officiels assez spécifiques pour être utile.Google Search Central documentation on hreflang is genuinely worth reading in full. It's one of those rare official docs that's specific enough to be useful.

---

Générer hreflang à l'échelle dans Next.js

Pour un site en 5 langues, vous pourriez écrire les balises hreflang à la main. Pour 12 locales sur 3 000 pages de produits, vous avez besoin d'une approche systématique.

Voici ce que j'utilise :

  1. Définissez votre liste de locales une fois, dans un seul fichier de configuration. Quelque chose comme locales.config.ts qui exporte un tableau : ['en', 'de', 'fr', 'es', 'nl'...].locales.config.ts exporting an array: ['en', 'de', 'fr', 'es', 'nl'...].
  2. Construisez une fonction utilitaire getHreflangAlternates(pathname: string) qui prend un chemin indépendant de la locale (/about ) et retourne un tableau d'objets { hreflang, href } pour chaque locale.getHreflangAlternates(pathname: string) that takes a locale-agnostic path (/about ) and returns an array of { hreflang, href } objects for every locale.
  3. Appelez cette fonction dans votre root layout.tsx et injectez les résultats via l'API metadata de Next.js en utilisant le champ alternates.languages. Cette fonctionnalité a été ajoutée dans Next.js 13.3 et elle génère automatiquement les balises <link rel="alternate" hreflang="..."> correctes dans <head>.layout.tsx and inject the results via Next.js's metadata API using the alternates.languages field. This was added in Next.js 13.3 and it generates the correct <link rel="alternate" hreflang="..."> tags in <head> automatically.
  4. Pour les pages avec du contenu dynamique (articles de blog, pages produit) où toutes les locales n'ont pas de traduction, passez un tableau supportedLocales à votre utilitaire et générez les balises uniquement pour les locales où le contenu existe réellement. Une balise hreflang pointant vers une page qui retourne 404 est pire qu'aucune balise hreflang.supportedLocales array to your utility and only generate tags for locales where the content actually exists. A hreflang tag pointing to a page that returns 404 is worse than no hreflang tag.

L'approche alternates.languages dans l'API metadata est le moyen le plus propre que j'ai trouvé pour gérer cela. Pas de manipulation custom de <Head>, pas de packages tiers pour les balises elles-mêmes.alternates.languages approach in the metadata API is the cleanest way I've found to handle this. No custom <Head> juggling, no third-party packages for the tags themselves.

---

Détection de locale sans être trop intrusif

Il y a un piège UX ici qui a aussi des implications SEO. La redirection automatique agressive basée sur la locale du navigateur est mauvaise pour les deux.

Si un utilisateur en Allemagne visite yoursite.com/en/article et que votre middleware le redirige en dur vers /de/artikel sans demander, vous venez de casser sa navigation intentionnelle. Vous avez également potentiellement cassé le partage de lien : quelqu'un partage l'URL en anglais, le destinataire en France est redirigé vers le français, et le contenu qu'il était censé voir n'existe peut-être pas encore en français.yoursite.com/en/article and your middleware hard-redirects them to /de/artikel without asking, you've just broken their intended navigation. You've also potentially broken link sharing: someone shares the English URL, the recipient in France gets bounced to French, and the content they were meant to see might not exist in French yet.

Ma règle : détectez, suggérez, ne forcez pas. Utilisez l'en-tête Accept-Language pour définir une locale par défaut sensée à la première visite (stockée dans un cookie), mais laissez les utilisateurs la remplacer manuellement et respectez ce remplacement à chaque visite suivante. Le cookie prend la priorité sur l'en-tête.detect, suggest, don't force. Use the Accept-Language header to set a sensible default on first visit (stored in a cookie), but let users override it manually and honour that override on every subsequent visit. The cookie takes priority over the header.

Pour le SEO spécifiquement : Googlebot n'envoie pas d'en-têtes Accept-Language significatifs. Il crawlera vos URLs telles quelles. Alors assurez-vous que chaque URL de locale est directement accessible sans redirection, et que votre sitemap inclut explicitement toutes les variantes de locale.Accept-Language headers. It will crawl your URLs as-is. So make sure every locale URL is directly accessible without redirection, and that your sitemap includes all locale variants explicitly.

La documentation du middleware next-intl couvre bien cette stratégie de détection, et c'est vers là que je dirigerais quiconque démarre un nouveau projet.next-intl middleware documentation covers this detection strategy well, and it's where I'd send anyone starting a new project.

---

Plans de site pour les sites multilingues

Un seul sitemap.xml qui inclut chaque variante de langue fonctionne bien pour les petits sites. Pour les grands sites (des dizaines de milliers d'URL sur plusieurs langues), je le divise : un index de sitemap à /sitemap.xml, puis des fichiers sitemap séparés par langue à /sitemaps/en.xml, /sitemaps/de.xml, et ainsi de suite.sitemap.xml that includes every locale variant is fine for smaller sites. For large sites (tens of thousands of URLs across multiple locales) I split it: one sitemap index at /sitemap.xml , then separate sitemap files per locale at /sitemaps/en.xml , /sitemaps/de.xml, and so on.

Chaque entrée d'URL doit inclure des balises <xhtml:link rel="alternate"> qui reflètent votre configuration hreflang. C'est redondant avec les balises in-page, mais cela aide Googlebot à faire le lien sur les pages qu'il n'a pas encore parcourues.<xhtml:link rel="alternate"> tags mirroring your hreflang setup. This is redundant with the in-page tags but it helps Googlebot connect the dots on pages it hasn't crawled yet.

Avec Next.js 14+, vous pouvez générer les sitemaps dynamiquement via app/sitemap.ts. Le type de retour supporte alternates.languages avec la même structure que l'API metadata. Connectez-le à la même fonction getHreflangAlternates que vous avez construite plus tôt et vous obtenez la cohérence gratuitement.app/sitemap.ts . The return type supports alternates.languages in the same shape as the metadata API. Wire it up to the same getHreflangAlternates utility you built earlier and you get consistency for free.

Un chiffre concret : sur un projet avec 8 langues et 4 500 produits, la division des sitemaps a réduit le temps de crawl moyen par langue d'environ 30% selon les statistiques de crawl de GSC. Pas révolutionnaire, mais significatif.

---

Bibliothèques de traduction : ce que j'utilise vraiment

Il y en a trois qui méritent d'être connues. next-intl est mon choix par défaut pour les projets App Router. Il a un support de première classe pour les server components, gère correctement la pluralisation, et l'API est propre. Je l'ai utilisée sur environ 40 projets depuis mi-2023.next-intl is my default for App Router projects. It has first-class support for server components, handles pluralisation properly, and the API is clean. I've used it on about 40 projects since mid-2023.

react-i18next est éprouvé au combat et dispose d'un écosystème immense, mais son modèle mental a été construit pour les client components et ajouter le support des server components via le cœur d'i18next semble bricolé comparé à l'approche native de next-intl.i18next core feels bolted-on compared to next-intl's native approach.

Lingui mérite le coup d'œil si votre workflow de traduction implique des traducteurs professionnels plutôt qu'un CMS. Son outillage d'extraction est supérieur. Mais pour la plupart des projets d'agence où le contenu provient de Contentful ou Sanity, next-intl est plus simple.

Ce que j'éviterais : développer le vôtre. Je l'ai fait une fois en 2020 pour un projet où le client insistait pour réutiliser directement ses fichiers de traduction PHP existants. Ça a marché, tant bien que mal, et j'ai passé deux ans à maintenir quelque chose que next-intl gère maintenant en un simple appel useTranslations().useTranslations() call.

---

FAQ

Next.js App Router supporte-t-il l'ancien bloc `i18n` dans `next.config.js` ?

Non. La clé de configuration i18n était spécifique au Pages Router. Si vous migrez vers App Router, vous devez implémenter le routage via un segment dynamique [locale] et un fichier middleware. L'ancien bloc de config est silencieusement ignoré dans les projets App Router.i18n configuration key was specific to the Pages Router. If you're migrating to App Router, you need to implement routing via a [locale] dynamic segment and a middleware file. The old config block is silently ignored in App Router projects.

Dois-je utiliser `lang` dans le chemin d'URL ou me fier aux sous-domaines ?

Pour la plupart des projets, lang dans le chemin d'URL (routage en sous-chemin comme /de/) est le bon choix. Les sous-domaines fragmentent votre autorité de domaine et nécessitent une configuration DNS séparée. Les ccTLD ne valent la complexité que si les signaux de confiance locale font réellement bouger les taux de conversion dans vos marchés spécifiques.lang in the URL path (sub-path routing like /de/) is the right call. Subdomains fragment your domain authority and require separate DNS configuration. ccTLDs are only worth the complexity if local trust signals are genuinely moving conversion rates in your specific markets.

Comment Googlebot gère-t-il le contenu de locale rendu en JavaScript ?

Mal, parfois. Si votre détection de locale s'exécute purement côté client dans un contexte React et que le HTML initial n'a aucun marqueur de locale, Googlebot peut indexer l'état pré-hydratation. Résolvez toujours la locale dans le middleware et assurez-vous que l'attribut `lang` correct est sur l'élément <html> dans le HTML rendu côté serveur. Vérifiez cela en affichant la source, pas en utilisant les devtools du navigateur (qui affichent l'état post-hydratation).lang attribute is on the <html> element in the server-rendered HTML. Check this by viewing source, not by using browser devtools (which show post-hydration state).

Quelle est la bonne valeur `x-default` hreflang pour un site sans page de sélecteur de langue ?

Pointez-la vers la page d'accueil de votre locale principale, généralement /en/. Cela signale à Google que l'anglais est votre langue de repli pour les utilisateurs dont vous ne supportez pas la langue. Ce n'est pas une pénalité de le faire, c'est juste un signal de préférence faible que Google peut ou non honorer./en/. It signals to Google that English is your fallback for users whose language you don't support. It's not a penalty to do this, it's just a soft preference signal Google may or may not honour.

Puis-je vérifier mon implémentation hreflang sans un outil SEO payant ?

Oui. L'outil Inspection des URL de Google Search Console vous affichera les balises hreflang détectées pour toute URL indexée. Il ne validera pas automatiquement la réciprocité, mais vous pouvez vérifier manuellement quelques pages représentatives. Pour la validation en masse, Screaming Frog (gratuit jusqu'à 500 URLs) gère les vérifications de réciprocité dans son onglet hreflang.Google Search Console's URL Inspection tool will show you detected hreflang tags for any indexed URL. It won't validate reciprocity automatically, but you can manually check a few representative pages. For bulk validation, Screaming Frog (free up to 500 URLs) handles reciprocity checks in its hreflang tab.

---

Bien faire du Next.js multilingue est un travail véritablement peu glamour. C'est des fichiers de configuration, de la cohérence d'URL, et s'assurer que deux développeurs n'ont pas implémenté la même chose deux fois dans des composants différents. Mais les sites où j'ai pris le temps de le faire correctement ont des classements qui tiennent bon sur les régions, et ceux où j'ai coupé les coins ont m'ont enseigné les leçons que j'ai écrites ici.

Le client berlinois, d'ailleurs, a finalement réussi à faire classer ses pages en allemand en Allemagne. Cela nous a pris un cycle de recrawl complet et beaucoup de café.

< BACK