Catégorie: Tutorial Dev

Next.js + Sanity CMS : gestion de contenu multilingue de zéro

Comment structurer un projet Next.js avec Sanity pour servir du contenu en plusieurs langues : conception du schéma, requêtes GROQ filtrées par langue, routes par locale et sélecteur de langue.

Veronica Cussi - 24 août 2026 - 3 min

Next.js + Sanity CMS : gestion de contenu multilingue de zéro

Quand un site doit servir du contenu en plusieurs langues, la tentation est de dupliquer tout le CMS par langue, ou de mettre toutes les langues dans un seul document avec un onglet par champ. Les deux options craquent dès que le site grandit. Voici la structure que j'utilise chez VeroLab avec Next.js et Sanity, et c'est celle qui fait tourner ce blog.

L'erreur la plus fréquente : des champs multilingues sur un seul document

Un pattern courant consiste à avoir un document unique avec title_es, title_en, title_fr, body_es, body_en, body_fr. Ça marche au début, mais devient ingérable dès que vous voulez publier une langue avant une autre, donner un SEO indépendant à chaque version, ou laisser un·e rédacteur·rice travailler uniquement en français sans toucher au reste. Le pattern qui passe mieux à l'échelle est un document par langue, reliés entre eux.

Le schéma : un document par langue + translationGroup

Chaque post a un champ language (es | en | fr) et un champ translationGroup, un identifiant partagé entre les trois versions du même article. Dans le schéma Sanity, ça ressemble à ça :

defineField({ name: 'language', type: 'string', options: { list: ['es', 'en', 'fr'] } }), defineField({ name: 'translationGroup', type: 'string' })

Ainsi, chaque langue est un document indépendant — avec son propre slug, son propre statut de publication et son propre flux éditorial — et translationGroup est la colle qui indique au frontend quels documents sont «la même page» dans une autre langue.

Requêtes GROQ filtrées par langue

Pour lister les posts d'une langue donnée, le filtre est direct : *[_type == "post" && language == $lang] | order(publishedAt desc). Pour le sélecteur de langue sur une page de détail, on cherche les autres documents partageant le même translationGroup : *[_type == "post" && translationGroup == $group && language != $lang]{ language, slug }.

Routes par locale dans Next.js

Avec l'App Router, la structure typique est app/[lang]/blog/[slug]/page.tsx. Dans generateStaticParams, vous générez toutes les combinaisons langue + slug en interrogeant Sanity, et sur la page vous résolvez le document en filtrant à la fois sur language et slug.current. Cela évite les collisions quand deux langues partagent le même slug par coïncidence.

SEO : hreflang sans prise de tête

Comme chaque langue est un document avec son propre slug, générer les balises hreflang se résume à une requête sur translationGroup au moment de générer les métadonnées de la page (generateMetadata), en reliant chaque version à sa vraie URL plutôt qu'en supposant un pattern fixe du type /es/, /en/, /fr/ devant le même slug.

Le sélecteur de langue côté frontend

Si un article n'a pas encore de traduction publiée dans une langue, le sélecteur n'affiche tout simplement pas cette option plutôt que de rediriger vers une 404 ou la page d'accueil. C'est une requête supplémentaire (celle sur translationGroup), mais ça évite une mauvaise expérience assez fréquente sur les sites multilingues mal conçus.

Pourquoi cette structure passe à l'échelle

Chaque langue peut être publiée, éditée ou même supprimée indépendamment sans toucher aux autres. Vous pouvez avoir un·e rédacteur·rice qui travaille uniquement en français. Et si vous ajoutez une quatrième langue demain, vous ne touchez pas au schéma des documents existants, vous ajoutez juste une option à la liste language.

Si vous montez un projet Next.js + Sanity multilingue et voulez un second avis sur l'architecture avant d'écrire du code, contactez-moi.

Articles liés

Comment migrer un projet Lovable vers Supabase étape par étape

Un guide pratique pour sortir un projet Lovable de son backend géré et le passer sur votre propre projet Supabase : schéma, auth, storage, RLS et déploiement, sans perdre de données en route.