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](https://wpmoderne.dev.wordpress-developpement.fr/wp-content/uploads/2023/03/slugs-traduits-wpml-routage-nextjs-multilingue-info-1024x512.jpg.webp)
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.