Cuando un sitio necesita servir contenido en varios idiomas, la tentación es duplicar el CMS entero por idioma o meter todos los idiomas en un solo campo con pestañas. Ambas opciones se rompen cuando el sitio crece. Esta es la estructura que uso en VeroLab con Next.js y Sanity, y es la misma que está detrás de este blog.
El error más común: campos multiidioma en un solo documento
Un patrón habitual es tener un documento único con title_es, title_en, title_fr, body_es, body_en, body_fr. Funciona al principio, pero se vuelve inmanejable en cuanto quieres publicar un idioma antes que otro, dar SEO independiente a cada versión, o dejar que un editor trabaje solo en francés sin tocar el resto. La alternativa que escala mejor es un documento por idioma, vinculados entre sí.
El schema: un documento por idioma + translationGroup
Cada post tiene un campo language (es | en | fr) y un campo translationGroup, un identificador compartido entre las tres versiones del mismo artículo. En el schema de Sanity queda algo así:
defineField({ name: 'language', type: 'string', options: { list: ['es', 'en', 'fr'] } }), defineField({ name: 'translationGroup', type: 'string' })
Con esto, cada idioma es un documento independiente —con su propio slug, su propio estado de publicación y su propio flujo editorial—, y translationGroup es el pegamento que le dice al frontend qué documentos son «la misma página» en otro idioma.
Consultas GROQ filtradas por idioma
Para listar los posts de un idioma concreto, el filtro es directo: *[_type == "post" && language == $lang] | order(publishedAt desc). Para el selector de idioma en una página de detalle, se busca el resto de documentos que comparten translationGroup: *[_type == "post" && translationGroup == $group && language != $lang]{ language, slug }.
Rutas por locale en Next.js
Con el App Router, la estructura típica es app/[lang]/blog/[slug]/page.tsx. En generateStaticParams generas todas las combinaciones idioma + slug consultando Sanity, y en la página resuelves el documento filtrando por language y slug.current a la vez. Esto evita colisiones cuando dos idiomas usan el mismo slug por coincidencia.
SEO: hreflang sin dolores de cabeza
Como cada idioma es un documento con su propio slug, generar las etiquetas hreflang es una simple consulta al translationGroup en el momento de generar los metadatos de la página (generateMetadata), enlazando cada versión con su URL real en lugar de asumir un patrón fijo como /es/, /en/, /fr/ delante del mismo slug.
El selector de idioma en el frontend
Si un artículo todavía no tiene traducción publicada en un idioma, el selector simplemente no muestra esa opción en vez de llevar a una página 404 o a la home. Es una consulta más (la del translationGroup) pero evita una mala experiencia bastante común en sitios multilingüe mal montados.
Por qué esta estructura escala
Cada idioma se puede publicar, editar o incluso borrar de forma independiente sin tocar los demás. Puedes tener un editor trabajando solo en francés. Y si mañana añades un cuarto idioma, no tocas el schema de los documentos existentes, solo añades una opción más a la lista de language.
Si estás montando un proyecto Next.js + Sanity multilingüe y quieres revisar la arquitectura antes de escribir código, escríbeme.
