vendredi 25 septembre 2026

À propos

Contact

Headless & API

Traduire les slugs d’URL en headless multilingue sans casser le routage Next.js

Un site WPML expose des slugs différents selon la langue, ce que le routage dynamique de Next.js doit résoudre correctement. Recette de mapping des routes par locale, sans reconfigurer WPML.

Par Clément Hadrot • 28 mars 2023 • 4 min de lecture • Aucun commentaire
Traduire les slugs d'URL en headless multilingue sans casser le routage Next.js

Sur un site institutionnel disponible en français, anglais et espagnol via WPML, l’équipe éditoriale a pris l’habitude de traduire aussi les slugs d’URL, pas seulement le contenu : l’article nos-valeurs en français devient our-values en anglais et nuestros-valores en espagnol. Cette pratique, excellente pour le référencement dans chaque langue, complique nettement le routage d’un front Next.js qui s’attend naïvement à un slug identique quelle que soit la locale. Cette recette ne traite pas la configuration de WPML elle-même, déjà couverte ailleurs sur ce blog, mais uniquement la résolution de ces slugs traduits côté front.

Le problème posé par un routage naïf

Un routage classique du type /[locale]/[slug]/page.jsx fonctionne très bien tant que l’URL suffit à elle seule à identifier le contenu recherché grâce à sa locale explicite. Le problème survient dès qu’il faut retrouver la version anglaise d’une page consultée en français : il ne suffit pas de remplacer fr par en dans l’URL, le slug complet change également. Un lien de bascule de langue mal codé renvoie alors une page introuvable, une erreur 404 sur une page qui existe pourtant bien dans l’autre langue.

La recette : une table de correspondance construite au build

WPML expose, via WPGraphQL et son extension WPML dédiée, la liste des traductions associées à chaque contenu. La solution consiste à interroger cette information une fois au build, pour construire une table de correspondance entre l’identifiant de contenu, sa locale et son slug traduit :

async function construireTableTraductions() {
  const { data } = await client.query({
    query: gql`
      query Traductions {
        pages(first: 500) {
          nodes {
            databaseId
            slug
            language { locale }
            translations {
              databaseId
              slug
              language { locale }
            }
          }
        }
      }
    `,
  })

  const table = {}
  for (const page of data.pages.nodes) {
    const groupe = { [page.language.locale]: page.slug }
    for (const traduction of page.translations) {
      groupe[traduction.language.locale] = traduction.slug
    }
    // Chaque locale de ce groupe pointe vers le même objet de correspondance
    for (const locale in groupe) {
      table[`${locale}:${groupe[locale]}`] = groupe
    }
  }
  return table
}
L'essentiel à retenir : Chaque langue peut avoir un slug totalement différent ; Un simple [locale]/[slug] ne suffit pas à résoudre l'ambiguïté ; Une table de correspondance construite au build évite les erreurs 404

Utiliser cette table pour le sélecteur de langue

Une fois cette table disponible, générée au build et injectée en tant que propriété statique de chaque page, le composant de bascule de langue devient trivial à écrire, sans jamais deviner un slug par transformation approximative :

function SelecteurLangue({ groupeTraductions, localeCourante }) {
  return (
    <nav>
      {Object.entries(groupeTraductions).map(([locale, slug]) => (
        locale !== localeCourante && (
          <a key={locale} href={`/${locale}/${slug}`}>
            {locale.toUpperCase()}
          </a>
        )
      ))}
    </nav>
  )
}

Résoudre le routage entrant, slug traduit compris

Côté génération des routes, generateStaticParams doit désormais parcourir chaque locale séparément, avec son propre slug traduit, plutôt que de supposer un slug commun à toutes les langues :

export async function generateStaticParams() {
  const table = await construireTableTraductions()
  const routes = []

  for (const cle in table) {
    const [locale, slug] = cle.split(':')
    routes.push({ locale, slug })
  }

  return routes
}

Le cas des pages sans traduction complète

Certaines pages n’existent pas encore dans toutes les langues du site, un cas fréquent pour un contenu fraîchement publié en français en attendant sa traduction. Le composant de bascule de langue vérifie systématiquement la présence de la locale ciblée dans le groupe de traductions avant d’afficher un lien, pour ne jamais proposer une page qui renverrait une erreur 404 :

{groupeTraductions[locale] ? (
  <a href={`/${locale}/${groupeTraductions[locale]}`}>{locale}</a>
) : (
  <span title="Traduction non disponible">{locale}</span>
)}

Les pièges à surveiller

  • Un slug traduit peut changer après publication si un rédacteur corrige une URL : la table de correspondance doit être régénérée à chaque revalidation de contenu, pas seulement au build initial.
  • Deux langues peuvent, par accident, partager exactement le même slug traduit : un test de build vérifie l’absence de collision avant chaque déploiement.
  • Le composant de bascule de langue ne doit jamais reconstruire un slug par simple remplacement de segment d’URL, une tentation fréquente qui casse dès la première page réellement traduite.

Un slug traduit est une bonne pratique de référencement multilingue, jamais un détail cosmétique pour le routage d’un front headless : il doit être traité comme une donnée à part entière, pas comme une variable d’URL interchangeable.

En résumé

Le routage dynamique de Next.js n’a rien d’incompatible avec des slugs traduits par WPML, à condition de construire une table de correspondance explicite entre locale et slug, plutôt que de supposer une structure d’URL uniforme d’une langue à l’autre. Cette table, une fois en place, simplifie autant la génération des routes que l’affichage fiable du sélecteur de langue, sans jamais renvoyer un visiteur vers une page inexistante.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi