Next.js 15 App Router + i18n statique : mon setup pour un portfolio trilingue SEO-friendly
Comment j'ai mis en place un portfolio trilingue (FR / EU / EN) avec Next.js 15 App Router, static params, hreflang, sitemap et canonical par locale - sans lib i18n lourde.
Mon portfolio doit exister en français, basque (euskara) et anglais. Pas pour faire joli : le Pays basque est un marché local fort, et mes clients internationaux lisent en anglais. Objectif : trois versions statiquement générées, hreflang correct, sitemap qui liste tout, et zéro dépendance i18n lourde.
Pourquoi pas next-intl ou react-i18next
Deux raisons. D'abord, next-intl tire tout un runtime côté client alors que 100 % de mon contenu est statique. Ensuite, react-i18next est pensé pour des apps SPA - pas pour le App Router qui préfère Server Components. Pour un site de 5 pages, du JSON plat et une convention de routing suffisent.
La structure
src/i18n/config.ts: liste des locales typéessrc/i18n/messages/{fr,eu,en}.json: dictionnairessrc/i18n/dictionaries.ts: loader paresseux par localesrc/app/[locale]/...: toutes les pages live sous[locale]middleware.ts: redirige/vers la locale par défaut
generateStaticParams + generateMetadata
Dans le layout [locale], on déclare les locales que Next doit précompiler :
export async function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}Et pour chaque page, une generateMetadata qui renseigne canonical + alternates.languages - c'est ce que Google lit pour hreflang :
alternates: {
canonical: `${SITE_URL}/${locale}/projets`,
languages: Object.fromEntries(
locales.map((l) => [l, `${SITE_URL}/${l}/projets`])
),
}Sitemap dynamique
src/app/sitemap.ts itère sur toutes les locales × toutes les pages et génère une entrée par combinaison, avec les alternates.languages pour chaque. Next compile ça en /sitemap.xml automatiquement.
OG locale
Attention au petit piège : openGraph.locale attend un code du type fr_FR, pas juste fr. J'ai un map dédié dans le layout pour traduire fr → fr_FR, eu → eu_ES, en → en_US.
Résultat
20 pages statiques (5 routes × 3 locales + /sitemap + /robots + opengraph-image dynamique), build en 1,4 s, aucun JavaScript i18n côté client, Lighthouse SEO à 100. Le code d'i18n tient en 3 fichiers. Pas besoin de plus.