Skip to main content
Next.js12 min

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ées
  • src/i18n/messages/{fr,eu,en}.json : dictionnaires
  • src/i18n/dictionaries.ts : loader paresseux par locale
  • src/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.