Cambiar tema

SEO multilingüe en Next.js: guía completa para que los buscadores indexen cada idioma correctamente

Easton editorial illustration: island architecture model

Ha creado cuidadosamente un sitio web multilingüe y, sin embargo, la búsqueda muestra la versión de idioma incorrecta. Los usuarios buscan en chino pero terminan en la página en inglés. Las versiones en diferentes idiomas compiten entre sí en los resultados de búsqueda y degradan la clasificación general.

Síntomas típicos de un SEO multilingüe mal configurado. Según las estadísticas de Google, más del 60% de los sitios web multilingües tienen errores de configuración de hreflang, con un impacto notable en la internacionalización y la experiencia del usuario.

60%+
Tasa de error en sitios multilingües
Más del 60 % de los sitios multilingües tienen errores de configuración hreflang

Este artículo muestra cómo implementar correctamente SEO multilingüe en Next.js:

  • configure las etiquetas hreflang correctamente – no confunda las versiones de idiomas
  • Genere mapas de sitio multilingües: acelere la indexación
  • Mejores prácticas de estructura de URL: elija la estrategia de internacionalización óptima
  • Encuentra y corrige errores típicos - localiza problemas rápidamente

Ya sea Pages Router o App Router, encontrará soluciones que se adapten a ambos.

1. Conceptos básicos de SEO multilingüe

1.1 ¿Qué es hreflang?

hreflang es un atributo HTML que indica a los motores de búsqueda el idioma de destino y la región de una página. Funciones principales:

  1. Evite sanciones por contenido duplicado: los motores de búsqueda reconocen esto: las versiones en diferentes idiomas son traducciones del mismo contenido, no contenido duplicado.
  2. Coincide con los usuarios con precisión: versión de idioma adecuada según el idioma y la región del usuario
  3. Mejora la UX: los usuarios no ven el idioma equivocado

1.2 Cómo maneja Google el contenido multilingüe

Al rastrear un sitio web multilingüe, Google hace lo siguiente:

  1. Detectar idioma (HTML-lang, hreflang, análisis de contenido)
  2. Evalúe las etiquetas hreflang y comprenda las relaciones entre las versiones de idiomas
  3. Mostrar la versión coincidente en los resultados de búsqueda.
  4. Agrupe las señales SEO en versiones lingüísticas (en lugar de dejarlas competir)

1.3 Errores típicos de SEO

Error 1: faltan etiquetas hreflang

<!-- ❌ Erreur : pas de balises hreflang -->
<head>
  <title>My Website</title>
  <link rel="canonical" href="https://example.com/en/about" />
</head>

Impacto: Los motores de búsqueda no reconocen las relaciones lingüísticas: pueden aparecer versiones de idiomas incorrectas en los resultados.

Error 2: configuración hreflang asimétrica

<!-- Page en anglais -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ❌ Page en chinois — erreur : hreflang manquant -->
<!-- Chaque version linguistique doit avoir des balises hreflang complètes -->

Impacto: Google requiere una configuración de hreflang simétrica: se ignora la información unilateral.

Error 3: códigos de idioma incorrectos

<!-- ❌ Erreur : codes de langue invalides -->
<link rel="alternate" hreflang="cn" href="..." /> <!-- correct : zh -->
<link rel="alternate" hreflang="en-us" href="..." /> <!-- correct : en-US, respecter la casse -->

Consecuencia: Los motores de búsqueda no reconocen los códigos - hreflang no funciona.## 2. Elija la estrategia de URL

Determine una estrategia de URL adecuada antes de la implementación: influye en el SEO, la UX y la implementación técnica.

2.1 Comparación de tres estrategias de URL comunes

EstrategiaEjemploImpacto SEOImplementaciónRecomendación
Subdirectorioejemplo.com/en/
ejemplo.com/zh/
⭐⭐⭐⭐⭐ Mejor⭐⭐⭐ Promedio⭐⭐⭐⭐⭐
Subdominiofr.ejemplo.com
zh.ejemplo.com
⭐⭐⭐ Promedio⭐⭐⭐⭐ Complejo⭐⭐⭐
Configuración de URLejemplo.com?lang=fr⭐⭐ Bajo⭐⭐⭐⭐⭐ Fácil⭐⭐

2.2 Estrategias en detalle

Opción 1: Subdirectorio (recomendado)

Beneficios :

  • Las señales de SEO se centran en el dominio principal: mejor clasificación general
  • Fácil configuración, sin dominios adicionales ni certificados SSL
  • Implementación mantenible, escalable y consistente.
  • Soporte nativo para Next.js

Desventajas:

  • Todos los idiomas comparten un dominio - sin configuración de DNS por mercado

Implementación de Next.js:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en', 'zh', 'ja', 'de'],
    defaultLocale: 'en',
    localeDetection: true // détection automatique de la langue utilisateur
  }
}

Opción 2: Subdominio

Beneficios :

  • Posibilidad de servidor separado por mercado (por ejemplo, China por separado)
  • Pila de tecnología independiente por subdominio
  • CDN más fácil y optimización geográfica

Desventajas:

  • Las señales SEO se distribuyen: cada subdominio necesita su propia clasificación
  • Gestión de dominio adicional y SSL
  • Mayores costos de implementación y mantenimiento.

Opción 3: parámetros de URL (no recomendado)

Beneficios :

  • Más fácil de implementar

Desventajas:

  • Peor efecto SEO: a menudo se ignoran las configuraciones
  • Mala UX, URL poco descriptiva
  • Almacenamiento en caché de CDN difícil
  • Las versiones lingüísticas son difíciles de distinguir en la búsqueda.

Conclusión :

Para la mayoría de los proyectos: Estrategia de subdirectorio. Mejor equilibrio entre SEO, esfuerzo y mantenimiento.## 3. hreflang en detalle

3.1 Eliminar etiquetas hreflang

hreflang le dice a los motores de búsqueda:

  1. ¿Qué versiones de idiomas están disponibles?
  2. la URL completa de cada versión
  3. Código de idioma y región por versión

3.2 hreflang en el enrutador de aplicaciones Next.js

Método 1: API de metadatos (recomendado)

Enrutador de aplicaciones Next.js 13+ con API de metadatos ligeros:

// app/[lang]/about/page.tsx
import { Metadata } from 'next'

type Props = {
  params: { lang: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang } = params

  // Langues prises en charge
  const languages = ['en', 'zh', 'ja', 'de']

  // Liens alternate pour toutes les langues
  const alternates = {
    canonical: `https://example.com/${lang}/about`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `https://example.com/${locale}/about`
      return acc
    }, {} as Record<string, string>)
  }

  return {
    title: 'About Us',
    alternates,
    // x-default pour les langues non correspondantes
    other: {
      'x-default': 'https://example.com/en/about'
    }
  }
}

export default function AboutPage({ params }: Props) {
  return <div>About page in {params.lang}</div>
}

Método 2: Limpiar el componente del cabezal

Para mayor control:

// components/I18nHead.tsx
import Head from 'next/head'

interface I18nHeadProps {
  currentLang: string
  pathname: string
  languages?: string[]
}

export default function I18nHead({
  currentLang,
  pathname,
  languages = ['en', 'zh', 'ja', 'de']
}: I18nHeadProps) {
  const baseUrl = 'https://example.com'

  return (
    <Head>
      {/* Canonical de la page courante */}
      <link rel="canonical" href={`${baseUrl}/${currentLang}${pathname}`} />

      {/* hreflang pour toutes les versions linguistiques */}
      {languages.map(lang => (
        <link
          key={lang}
          rel="alternate"
          hrefLang={lang}
          href={`${baseUrl}/${lang}${pathname}`}
        />
      ))}

      {/* x-default vers la langue par défaut */}
      <link
        rel="alternate"
        hrefLang="x-default"
        href={`${baseUrl}/en${pathname}`}
      />
    </Head>
  )
}

Usar :

// app/[lang]/about/page.tsx
import I18nHead from '@/components/I18nHead'

export default function AboutPage({ params }: { params: { lang: string } }) {
  return (
    <>
      <I18nHead
        currentLang={params.lang}
        pathname="/about"
      />
      <div>About page content</div>
    </>
  )
}

3.3 hreflang en el enrutador de página Next.js

Pages Router utiliza otras API:

// pages/about.tsx
import { GetStaticProps } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'

export default function AboutPage() {
  const router = useRouter()
  const { locale, locales, asPath } = router
  const baseUrl = 'https://example.com'

  return (
    <>
      <Head>
        {/* Canonical de la page courante */}
        <link rel="canonical" href={`${baseUrl}/${locale}${asPath}`} />

        {/* hreflang pour toutes les versions linguistiques */}
        {locales?.map(loc => (
          <link
            key={loc}
            rel="alternate"
            hrefLang={loc}
            href={`${baseUrl}/${loc}${asPath}`}
          />
        ))}

        {/* x-default langue par défaut */}
        <link
          rel="alternate"
          hrefLang="x-default"
          href={`${baseUrl}/en${asPath}`}
        />
      </Head>

      <div>About page content</div>
    </>
  )
}

export const getStaticProps: GetStaticProps = async ({ locale }) => {
  return {
    props: {
      messages: (await import(`../locales/${locale}.json`)).default
    }
  }
}

3.4 Códigos regionales (REGION-idioma)

Para contenido específico de un país, el formato “idioma-REGIÓN”:

// Anglais pour différentes régions
const hreflangConfig = {
  'en-US': 'https://example.com/en-us/about', // US-Englisch
  'en-GB': 'https://example.com/en-gb/about', // UK-Englisch
  'en-AU': 'https://example.com/en-au/about', // Australien
  'zh-CN': 'https://example.com/zh-cn/about', // Festlandchina vereinfachtes Chinesisch
  'zh-TW': 'https://example.com/zh-tw/about', // Taiwan traditionelles Chinesisch
  'zh-HK': 'https://example.com/zh-hk/about', // Hongkong traditionelles Chinesisch
}

Enrutamiento regional en Next.js:

// next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'en-GB', 'en-AU', 'zh-CN', 'zh-TW', 'zh-HK'],
    defaultLocale: 'en-US',
  }
}

3.5 Errores de configuración típicos y correcciones

Error 1: Falta de autorreferencia

<!-- ❌ Erreur : la page courante ne se référence pas elle-même -->
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ✅ Correct : auto-référence obligatoire -->
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

¿Por qué autorreferencia?
Google requiere un hreflang simétrico: cada versión de idioma debe hacer referencia a todos los demás, incluido él mismo.

Error 2: falta x-default

<!-- ✅ Recommandé : x-default comme repli -->
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />

x-default renvoie la version par défaut pour les langues non prises en charge, par ex. Par exemple :

  • Utilisateurs avec une langue de navigateur arabe, site Web sans arabe
  • Le moteur de recherche renvoie la page spécifiée sous x-default

Error 3: hreflang y canónico en conflicto

<!-- ❌ Erreur : canonical pointe vers une autre langue -->
<link rel="canonical" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

<!-- ✅ Correct : canonical pointe vers la version linguistique courante -->
<link rel="canonical" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />

Regla general: canónica debe ser la URL de la página actual, nunca una versión en otro idioma.## 4. Mapa del sitio multilingüe

Los mapas de sitio ayudan a los motores de búsqueda a encontrar e indexar páginas. Para sitios multilingües, la configuración adecuada del mapa del sitio es fundamental.

4.1 ¿Por qué un mapa del sitio multilingüe?

Tres ventajas:

  1. Indexación más rápida: informe activamente de todas las versiones de idiomas en lugar de esperar al rastreador.
  2. Integridad: no se pasa por alto ninguna versión de idioma, especialmente con estructuras de enlaces profundos.
  3. hreflang en el mapa del sitio: fortalece aún más las relaciones lingüísticas

4.2 Estrategia del mapa del sitio

Dependiendo del tamaño del sitio:

Opción 1: un mapa del sitio (sitios pequeños)

Todas las URL de idiomas en un sitemap.xml: adecuado para hasta aprox. 5.000 páginas:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <!-- Version anglaise -->
  <url>
    <loc>https://example.com/en/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
    <xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
  <!-- Version chinoise -->
  <url>
    <loc>https://example.com/zh/about</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://example.com/en/about"/>
    <xhtml:link rel="alternate" hreflang="zh" href="https://example.com/zh/about"/>
    <xhtml:link rel="alternate" hreflang="ja" href="https://example.com/ja/about"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://example.com/en/about"/>
  </url>
</urlset>

Opción 2: Mapa del sitio por idioma (sitios grandes)

Un mapa del sitio por idioma, agrupado mediante un índice de mapas del sitio, de alrededor de 5000 páginas o muchos idiomas:

<!-- sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>https://example.com/sitemap-en.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-zh.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
  <sitemap>
    <loc>https://example.com/sitemap-ja.xml</loc>
    <lastmod>2025-01-01</lastmod>
  </sitemap>
</sitemapindex>

4.3 Mapa del sitio en el enrutador de aplicaciones Next.js

Next.js 13+ con generación de mapas de sitio integrada:

// app/sitemap.ts
import { MetadataRoute } from 'next'

const languages = ['en', 'zh', 'ja', 'de']
const routes = ['', '/about', '/blog', '/contact']

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://example.com'
  const sitemap: MetadataRoute.Sitemap = []

  routes.forEach(route => {
    languages.forEach(lang => {
      const url = `${baseUrl}/${lang}${route}`

      sitemap.push({
        url,
        lastModified: new Date(),
        changeFrequency: 'weekly',
        priority: route === '' ? 1 : 0.8,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${route}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

4.4 Mapa del sitio para contenido dinámico

Para publicaciones de blog o productos DB/CMS:

// app/sitemap.ts
import { MetadataRoute } from 'next'

const languages = ['en', 'zh', 'ja']
const baseUrl = 'https://example.com'

async function getArticles() {
  return [
    { slug: 'getting-started', lastModified: '2025-01-01' },
    { slug: 'advanced-guide', lastModified: '2025-01-15' },
  ]
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const sitemap: MetadataRoute.Sitemap = []

  const staticPages = ['', '/about', '/contact']
  staticPages.forEach(page => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}${page}`,
        lastModified: new Date(),
        changeFrequency: 'monthly',
        priority: page === '' ? 1 : 0.8,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${page}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  const articles = await getArticles()
  articles.forEach(article => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}/blog/${article.slug}`,
        lastModified: new Date(article.lastModified),
        changeFrequency: 'weekly',
        priority: 0.6,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

4.5 Mapa del sitio en Pages Router

Manualmente a través de la ruta API:

// pages/api/sitemap.xml.ts
import { NextApiRequest, NextApiResponse } from 'next'

const baseUrl = 'https://example.com'
const languages = ['en', 'zh', 'ja']

function generateSiteMap(pages: string[]) {
  return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
${pages.map(page => {
  return languages.map(lang => {
    const url = `${baseUrl}/${lang}${page}`
    const alternates = languages.map(l =>
      `    <xhtml:link rel="alternate" hreflang="${l}" href="${baseUrl}/${l}${page}"/>`
    ).join('\n')

    return `  <url>
    <loc>${url}</loc>
    <lastmod>${new Date().toISOString()}</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
${alternates}
    <xhtml:link rel="alternate" hreflang="x-default" href="${baseUrl}/en${page}"/>
  </url>`
  }).join('\n')
}).join('\n')}
</urlset>`
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const pages = ['', '/about', '/blog', '/contact']
  const sitemap = generateSiteMap(pages)

  res.setHeader('Content-Type', 'text/xml')
  res.write(sitemap)
  res.end()
}

4.6 Enviar el mapa del sitio a los motores de búsqueda

Enviar activamente después de la generación:

Método 1: declarar en robots.txt

Variante más simple: los robots leen automáticamente:

# public/robots.txt
User-agent: *
Allow: /

Sitemap: https://example.com/sitemap.xml

Método 2: Consola de búsqueda de Google

El envío manual acelera la indexación:

  1. Abra Google Search Console.
  2. Seleccione la propiedad
  3. Menú de mapas del sitio
  4. Ingrese la URL.xml del mapa del sitio.
  5. Haga clic en “Enviar”.

Método 3: Herramientas para webmasters de Bing

Bing tiene una cuota de mercado relevante:

  1. Abra [Herramientas para webmasters de Bing] (https://www.bing.com/webmasters).
  2. Agregar un sitio web
  3. Envíe el mapa del sitio en la sección “Mapas del sitio”.

4.7 Validar el plano del sitio

Herramientas de validación:

  1. Validador de mapas de sitios XML: https://www.xml-sitemaps.com/validate-xml-sitemap.html
  2. Google Search Console: estado del índice y errores después del envío
  3. Validador XML en línea: Verifique el cumplimiento de los estándares## 5. Mejores prácticas y notas

5.1 Calidad de las traducciones

Google reconoce las traducciones de mala calidad, lo que afecta a las clasificaciones.

Evitar :

  • ❌ Publicar traducción automática directamente (Google Translate)
  • ❌ Traducir solo la navegación y el título, dejar el cuerpo en un solo idioma
  • ❌ Diferencias significativas en el contenido entre las versiones lingüísticas

Recomendado :

  • ✅ Traductores profesionales o hablantes nativos
  • ✅ Localización en lugar de pura traducción (cultura, expresión)
  • ✅ Estructura uniforme y calidad de todas las versiones lingüísticas.

5.2 Riesgo SEO vinculado a la traducción automática del cliente

La traducción automática del lado del cliente no funciona para SEO: los rastreadores ven el contenido original:

// ❌ Non recommandé : traduction automatique côté client (non indexable)
import GoogleTranslate from 'google-translate-api'

export default function Page() {
  const [content, setContent] = useState('')

  useEffect(() => {
    GoogleTranslate(originalText, { to: 'zh' })
      .then(res => setContent(res.text))
  }, [])

  return <div>{content}</div>
}
// ✅ Recommandé : SSR avec de vraies traductions
export default function Page({ params }: { params: { lang: string } }) {
  const content = await getTranslatedContent(params.lang)

  return <div>{content}</div>
}

5.3 Rendimiento

Los sitios multilingües llegan a usuarios de todo el mundo: el rendimiento importa.

1. CDN multirregional

// next.config.js
module.exports = {
  images: {
    domains: ['cdn.example.com'],
  },
  compress: true,
}

2. Carga diferida de paquetes de idiomas

No cargue todos los archivos de traducción a la vez:

const messages = await import(`@/locales/${lang}.json`)

3. Estrategia de caché

// app/[lang]/layout.tsx
export const revalidate = 3600 // stündliche Revalidierung

5.4 Monitoreo y mantenimiento

El SEO multilingüe no es una tarea de una sola vez.

1. Verifique si hay errores de hreflang

Informe de orientación internacional de Google Search Console:

  • errores y advertencias de hreflang
  • Estado del índice por idioma
  • Rendimiento de búsqueda y tasas de clics por idioma

2. Herramientas recomendadas

3. Guión de seguimiento

Comprobación automatizada de hreflang:

// scripts/check-hreflang.ts
import { JSDOM } from 'jsdom'

async function checkHreflang(url: string) {
  const response = await fetch(url)
  const html = await response.text()
  const dom = new JSDOM(html)
  const document = dom.window.document

  const hreflangLinks = document.querySelectorAll('link[rel="alternate"][hreflang]')

  console.log(`Found ${hreflangLinks.length} hreflang links on ${url}`)

  hreflangLinks.forEach(link => {
    const hreflang = link.getAttribute('hreflang')
    const href = link.getAttribute('href')
    console.log(`  ${hreflang}: ${href}`)
  })

  const currentUrl = new URL(url).href
  const hasSelfReference = Array.from(hreflangLinks).some(
    link => link.getAttribute('href') === currentUrl
  )

  if (!hasSelfReference) {
    console.warn('⚠️ Warning: Missing self-reference hreflang tag')
  }

  const hasXDefault = Array.from(hreflangLinks).some(
    link => link.getAttribute('hreflang') === 'x-default'
  )

  if (!hasXDefault) {
    console.warn('⚠️ Warning: Missing x-default hreflang tag')
  }
}

checkHreflang('https://example.com/en/about')
checkHreflang('https://example.com/zh/about')
```## 6. Exemple pratique : projet complet

Cómo implementar SEO multilingüe en un proyecto de Next.js App Router.

### 6.1 Estructura del proyecto

my-i18n-site/
├── app/
│ ├── [lang]/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── about/
│ │ │ └── page.tsx
│ │ └── blog/
│ │ ├── page.tsx
│ │ └── [slug]/
│ │ └── page.tsx
│ ├── sitemap.ts
│ └── robots.ts
├── components/
│ └── I18nMetadata.tsx
├── lib/
│ ├── i18n.ts
│ └── articles.ts
├── locales/
│ ├── en.json
│ ├── zh.json
│ └── ja.json
└── next.config.js


### 6.2 Configuración

```javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // App Router n'utilise pas i18n dans next.config — routage manuel
}

module.exports = nextConfig
// lib/i18n.ts
export const languages = ['en', 'zh', 'ja'] as const
export type Language = (typeof languages)[number]

export const defaultLanguage: Language = 'en'

export const languageNames: Record<Language, string> = {
  en: 'English',
  zh: '中文',
  ja: '日本語',
}

export function isValidLanguage(lang: string): lang is Language {
  return languages.includes(lang as Language)
}

6.3 Componente de diseño

// app/[lang]/layout.tsx
import { languages, isValidLanguage } from '@/lib/i18n'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  return languages.map(lang => ({ lang }))
}

export default function LangLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: { lang: string }
}) {
  if (!isValidLanguage(params.lang)) {
    notFound()
  }

  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  )
}

6.4 Página de metadatos

// app/[lang]/about/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'

type Props = {
  params: { lang: Language }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang } = params
  const baseUrl = 'https://example.com'
  const pathname = '/about'

  const alternates = {
    canonical: `${baseUrl}/${lang}${pathname}`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `${baseUrl}/${locale}${pathname}`
      return acc
    }, {} as Record<string, string>)
  }

  const titles: Record<Language, string> = {
    en: 'About Us - Learn More About Our Company',
    zh: '关于我们 - 了解更多关于我们公司的信息',
    ja: '私たちについて - 当社についてもっと知る',
  }

  const descriptions: Record<Language, string> = {
    en: 'Learn about our mission, values, and the team behind our success.',
    zh: '了解我们的使命、价值观以及我们成功背后的团队。',
    ja: '私たちの使命、価値観、そして成功を支えるチームについて学びます。',
  }

  return {
    title: titles[lang],
    description: descriptions[lang],
    alternates,
    openGraph: {
      title: titles[lang],
      description: descriptions[lang],
      url: `${baseUrl}/${lang}${pathname}`,
      siteName: 'Example Site',
      locale: lang,
      type: 'website',
    },
  }
}

export default function AboutPage({ params }: Props) {
  const content = {
    en: 'About us content in English...',
    zh: '关于我们的中文内容...',
    ja: '私たちについての日本語コンテンツ...',
  }

  return (
    <div>
      <h1>About Us</h1>
      <p>{content[params.lang]}</p>
    </div>
  )
}

6.5 Ruta dinámica con hreflang

// app/[lang]/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { languages, Language } from '@/lib/i18n'
import { getArticle, getAllArticles } from '@/lib/articles'
import { notFound } from 'next/navigation'

type Props = {
  params: { lang: Language; slug: string }
}

export async function generateStaticParams() {
  const articles = await getAllArticles()

  return languages.flatMap(lang =>
    articles.map(article => ({
      lang,
      slug: article.slug,
    }))
  )
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang, slug } = params
  const article = await getArticle(slug, lang)

  if (!article) {
    return {}
  }

  const baseUrl = 'https://example.com'
  const pathname = `/blog/${slug}`

  const alternates = {
    canonical: `${baseUrl}/${lang}${pathname}`,
    languages: languages.reduce((acc, locale) => {
      acc[locale] = `${baseUrl}/${locale}${pathname}`
      return acc
    }, {} as Record<string, string>)
  }

  return {
    title: article.title,
    description: article.excerpt,
    alternates,
    openGraph: {
      title: article.title,
      description: article.excerpt,
      url: `${baseUrl}/${lang}${pathname}`,
      type: 'article',
      publishedTime: article.publishedAt,
      authors: [article.author],
    },
  }
}

export default async function BlogArticle({ params }: Props) {
  const { lang, slug } = params
  const article = await getArticle(slug, lang)

  if (!article) {
    notFound()
  }

  return (
    <article>
      <h1>{article.title}</h1>
      <p>{article.content}</p>
    </article>
  )
}

6.6 Generación de mapas del sitio

// app/sitemap.ts
import { MetadataRoute } from 'next'
import { languages } from '@/lib/i18n'
import { getAllArticles } from '@/lib/articles'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://example.com'
  const sitemap: MetadataRoute.Sitemap = []

  const staticPages = ['', '/about', '/contact']
  staticPages.forEach(page => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}${page}`,
        lastModified: new Date(),
        changeFrequency: 'monthly',
        priority: page === '' ? 1 : 0.8,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}${page}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  const articles = await getAllArticles()
  articles.forEach(article => {
    languages.forEach(lang => {
      sitemap.push({
        url: `${baseUrl}/${lang}/blog/${article.slug}`,
        lastModified: new Date(article.updatedAt),
        changeFrequency: 'weekly',
        priority: 0.6,
        alternates: {
          languages: languages.reduce((acc, l) => {
            acc[l] = `${baseUrl}/${l}/blog/${article.slug}`
            return acc
          }, {} as Record<string, string>)
        }
      })
    })
  })

  return sitemap
}

6.7 Robots.txt

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
    },
    sitemap: 'https://example.com/sitemap.xml',
  }
}
```## 7. Validation et tests

### 7.1 Lista de verificación local

Antes de la implementación en producción:

- [ ] `<html lang="...">` correcto en todas las páginas
- [] Etiquetas hreflang completas en cada página (todas las versiones de idiomas)
- [] Autoreferencia presente en hreflang
- [ ] `x-default` apunta al idioma predeterminado
- [] puntos canónicos a la versión del idioma actual
- [] El mapa del sitio contiene todas las URL de idiomas
- [] robots.txt apunta a sitemap.xml
- [] Calidad de traducción uniforme de todas las versiones de idiomas

### 7.2 Prueba de resultados enriquecidos de Google

[Prueba de resultados enriquecidos de Google](https://search.google.com/test/rich-results):

1. Ingrese la URL de la página
2. Espere la exploración y el análisis.
3. Compruebe si hay errores y advertencias.
4. Verifique el reconocimiento de hreflang

### 7.3 herramientas de inspección hreflang

- **Aleyda Solis generador de hreflang**: https://www.aleydasolis.com/english/international-seo-tools/hreflang-tags-generator/
- **Comprobador Merkle hreflang**: https://technicalseo.com/tools/hreflang/

Características:
- Comprobación por lotes de varias páginas.
- Identificar problemas de simetría.
- Verifique si hay errores en el código de idioma

### 7.4 Consola de búsqueda de Google

Después del despliegue:

1. Envíe el mapa del sitio a Search Console.
2. Espere de 1 a 2 semanas para la primera indexación.
3. Marque “Orientación internacional” > “Idioma”.
4. Corregir errores y advertencias de hreflang
5. Supervisar el rendimiento de la búsqueda por idioma## 8. Preguntas frecuentes

### P1: ¿Diferencia entre hreflang y canónico?

- **canonical** – URL canónica de la página, contra contenido duplicado
- **hreflang**: versiones de idiomas disponibles, para orientación por idioma

Los dos se pueden combinar. canonical por versión de idioma en sí mismo, hreflang en todas las versiones.

### P2: ¿Hreflang obligatorio en cada página?

Sí. En **cualquier** versión de idioma, simétrica (referencia mutua). La página en inglés no es suficiente: Google la ignora.

### P3: ¿Adónde apunta x-default?

Principalmente lenguaje estándar o versión más general:

- Audiencia principal Inglés → Versión en inglés
- Sitio mundial → Inglés internacional (en-US)
- Sitio regional → Idioma principal de la región

### P4: ¿Subdirectorio o subdominio?

**Subdirectorio (recomendado):**
- SEO en el dominio principal
- Fácil implementación
- Para la mayoría de los proyectos

**Subdominio:**
- Servidor propio por mercado
- Grandes sitios internacionales con sus propias operaciones de mercado.
- Costos de dominio adicionales

**Conclusión:** No hay requisitos especiales → Subdirectorio.

### P5: ¿Contenido traducido automáticamente?

No recomendado para SEO:

- Google detecta traducciones débiles
- Menor valor del contenido.
- Mala experiencia de usuario, alta tasa de rebote

Si tienes un presupuesto ajustado:

1. Primero las páginas principales (página de inicio, productos principales, tráfico principal)
2. Edición MT + humana
3. Mejorar gradualmente la calidad

### P6: ¿Cuánto tiempo falta para la indexación?

Horario típico:

- Después de enviar el mapa del sitio **1 a 2 semanas** primera indexación
- Indexación completa **1-2 meses**
- Señales SEO **3-6 meses**

Acelerar:
- Mapa del sitio correcto + envío
- Contenido y actualizaciones de alta calidad.
- Vínculos de retroceso de calidad
- Solicitar indexación de páginas importantes en Search Console## 9. Resumen

El SEO multilingüe es la clave del éxito internacional. Puntos clave:### 9.1 Puntos clave

1. **Estrategia de URL**
   - Subdirectorio recomendado (ejemplo.com/en/, ejemplo.com/zh/)
   - Estructura de URL clara y consistente

2. **hreflang**
   - Etiquetas completas en cada página.
   - Autorreferencia obligatoria
   - x-predeterminado al idioma predeterminado
   - Códigos de idioma ISO 639-1

3. **Mapa del sitio**
   - Todas las versiones de idiomas incluidas.
   - HReflang opcional en el mapa del sitio
   - Actualizar y enviar periódicamente

4. **Calidad del contenido**
   - Sin versión MT sin formato
   - Consistencia y profesionalismo.
   - Localización, no sólo traducción

5. **Monitoreo**
   - Consola de búsqueda de Google
   - Control periódico del hreflang
   - Seguimiento del rendimiento por idioma

### 9.2 Lista de acciones

- [] Seleccionar e implementar estrategia de URL (subdirectorio recomendado)
- [] Etiquetas hreflang completas en todas las páginas
- [] Corregir etiquetas canónicas
- [] Generar un mapa del sitio multilingüe
- [ ] robots.txt con referencia al mapa del sitio
- [] Enviar mapa del sitio a Google Search Console y Bing
- [] Verifique el hreflang con herramientas de validación
- [ ] Garantizar la calidad de la traducción.
- [ ] Implementar monitoreo y revisiones periódicas

### 9.3 Para ir más lejos

- [Google: sitios multilingües y multirregionales](https://developers.google.com/search/docs/specialty/international)
- [Google: guía hreflang](https://developers.google.com/search/docs/specialty/international/localized-versions)
- [Next.js: internacionalización](https://nextjs.org/docs/app/building-your-application/routing/internationalization)
- [Schema.org: marcado multilingüe](https://schema.org/docs/multilingual.html)

Un SEO multilingüe bien configurado lleva tiempo, pero el retorno es significativo: mejores clasificaciones, usuarios mejor orientados y mayor conversión. Si sigue estas mejores prácticas, su sitio tendrá un rendimiento significativamente mejor en los motores de búsqueda.





Si tienes dudas durante la implementación, ¡comenta y lo vemos juntos!

Flujo completo de configuración SEO multilingüe en Next.js

Pasos completos: etiquetas hreflang, sitemap multilingüe y elección de estrategia de URL

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Configurar etiquetas hreflang

    Configura en metadata:
    ```tsx
    // app/[locale]/about/page.tsx
    export async function generateMetadata({ params }): Promise<Metadata> {
    const { locale } = params

    return {
    title: 'About Us',
    alternates: {
    languages: {
    'zh': '/zh/about',
    'en': '/en/about',
    'x-default': '/en/about', // idioma por defecto
    },
    },
    }
    }
    ```

    Puntos clave:
    • Incluir todas las versiones de idioma
    • x-default apunta al idioma por defecto
    • Configurar en cada página

    Salida HTML:
    ```html
    <link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
    <link rel="alternate" hreflang="en" href="https://example.com/en/about" />
    <link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
    ```

    Efecto:
    • Indicar el idioma objetivo a los motores de búsqueda
    • Evitar penalización por contenido duplicado
    • Emparejar mejor al usuario
  2. 2

    Step 2: Generar sitemap multilingüe

    Método 1: generar un sitemap independiente por idioma
    ```tsx
    // app/[locale]/sitemap.ts
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const baseUrl = 'https://example.com'
    const locale = params.locale

    return [
    {
    url: `${baseUrl}/${locale}`,
    lastModified: new Date(),
    changeFrequency: 'daily',
    priority: 1,
    },
    // ...
    ]
    }
    ```

    Método 2: usar un índice de sitemaps
    ```tsx
    // app/sitemap.ts
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const locales = ['zh', 'en']
    const baseUrl = 'https://example.com'

    return locales.flatMap(locale => [
    {
    url: `${baseUrl}/${locale}`,
    lastModified: new Date(),
    changeFrequency: 'daily',
    priority: 1,
    },
    // ...
    ])
    }
    ```

    Puntos clave:
    • Incluir todas las versiones de idioma
    • Usar el formato de URL correcto
    • Enviar a Google Search Console
  3. 3

    Step 3: Elegir estrategia de URL

    Opción 1: subdirectorio (recomendado)
    • Formato de URL: /zh/about, /en/about
    • Configuración simple
    • SEO-friendly
    • Adecuado para la mayoría de proyectos

    Opción 2: subdominio
    • Formato de URL: zh.example.com, en.example.com
    • Requiere configurar varios dominios
    • Más profesional
    • Adecuado para proyectos grandes

    Opción 3: Cookie
    • Cambiar idioma mediante cookie
    • La URL no incluye prefijo de idioma
    • No es SEO-friendly
    • No recomendado

    Recomendación:
    • La mayoría de proyectos → subdirectorio
    • Proyectos grandes → subdominio
    • Evitar → Cookie

    Punto clave: el subdirectorio es la opción más SEO-friendly.
  4. 4

    Step 4: Validar y probar

    Herramientas de validación:

    1. Google Search Console:
    • Enviar el sitemap multilingüe
    • Revisar etiquetas hreflang
    • Consultar el estado de indexación

    2. Herramientas de prueba hreflang:
    • https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
    • Comprobar si la configuración hreflang es correcta

    3. Validación del sitemap multilingüe:
    • Revisar el formato del sitemap
    • Confirmar que incluye todos los idiomas
    • Verificar que las URL son correctas

    Errores frecuentes:
    • Etiquetas hreflang ausentes
    • x-default mal configurado
    • Sitemap sin todas las versiones de idioma
    • Formato de URL inconsistente

    Consejo: valida justo después de configurar, no esperes a que aparezcan problemas.

FAQ

¿Qué es la etiqueta hreflang? ¿Por qué la necesitas?
hreflang es un atributo HTML que indica a los motores de búsqueda el idioma y la región objetivo de una página.

Funciones principales:
1. Evitar penalización por contenido duplicado: las versiones en distintos idiomas son traducciones del mismo contenido, no duplicados
2. Emparejar mejor al usuario: mostrar la versión más adecuada según idioma y región
3. Mejorar la UX: evitar que el usuario vea el idioma equivocado

Método de configuración:
```tsx
export async function generateMetadata({ params }): Promise<Metadata> {
return {
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about',
},
},
}
}
```

Puntos clave:
• Incluir todas las versiones de idioma
• x-default apunta al idioma por defecto
• Configurar en cada página

Según Google, más del 60 % de los sitios multilingües tienen errores de configuración hreflang.
¿Cómo configurar las etiquetas hreflang?
Configura en metadata:
```tsx
// app/[locale]/about/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale } = params

return {
title: 'About Us',
alternates: {
languages: {
'zh': '/zh/about',
'en': '/en/about',
'x-default': '/en/about', // idioma por defecto
},
},
}
}
```

Salida HTML:
```html
<link rel="alternate" hreflang="zh" href="https://example.com/zh/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />
```

Puntos clave:
• Incluir todas las versiones de idioma
• x-default apunta al idioma por defecto
• Configurar en cada página
• Las URL deben ser absolutas

Nota: hreflang debe incluir todas las versiones, incluida la página actual.
¿Cómo generar un sitemap multilingüe?
Método 1: generar un sitemap independiente por idioma
```tsx
// app/[locale]/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com'
const locale = params.locale

return [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
]
}
```

Método 2: usar un índice de sitemaps
```tsx
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const locales = ['zh', 'en']
const baseUrl = 'https://example.com'

return locales.flatMap(locale => [
{
url: `${baseUrl}/${locale}`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
])
}
```

Puntos clave:
• Incluir todas las versiones de idioma
• Usar el formato de URL correcto
• Enviar a Google Search Console

Recomendación: usar un índice de sitemaps para mayor flexibilidad.
¿Cómo elegir la estrategia de URL de un sitio multilingüe?
Tres estrategias de URL:

Opción 1: subdirectorio (recomendado)
• Formato: /zh/about, /en/about
• Configuración simple
• SEO-friendly
• Adecuado para la mayoría de proyectos

Opción 2: subdominio
• Formato: zh.example.com, en.example.com
• Requiere varios dominios
• Más profesional
• Adecuado para proyectos grandes

Opción 3: Cookie
• Cambiar idioma mediante cookie
• La URL no incluye prefijo de idioma
• No es SEO-friendly
• No recomendado

Recomendación:
• La mayoría de proyectos → subdirectorio
• Proyectos grandes → subdominio
• Evitar → Cookie

Punto clave: el subdirectorio es la opción más SEO-friendly.

Nota: tras elegir la estrategia, las URL de hreflang deben seguir el mismo formato.
¿Cómo validar la configuración SEO multilingüe?
Herramientas de validación:

1. Google Search Console:
• Enviar el sitemap multilingüe
• Revisar etiquetas hreflang
• Consultar el estado de indexación

2. Herramientas de prueba hreflang:
• https://www.aleydasolis.com/en/english-tools/international-seo-tools/hreflang-tags-validator/
• Comprobar si la configuración hreflang es correcta

3. Validación del sitemap multilingüe:
• Revisar el formato del sitemap
• Confirmar que incluye todos los idiomas
• Verificar que las URL son correctas

Errores frecuentes:
• Etiquetas hreflang ausentes
• x-default mal configurado
• Sitemap sin todas las versiones de idioma
• Formato de URL inconsistente

Recomendaciones:
• Validar justo después de configurar
• Revisar el estado de indexación con regularidad
• Corregir problemas a tiempo

Recuerda: la validación es una parte esencial de la optimización SEO.
¿Cuáles son los errores frecuentes en SEO multilingüe?
Errores comunes:

1. Faltan etiquetas hreflang
• El motor de búsqueda no conoce el idioma de la página
• Puede mostrarse la versión equivocada

2. x-default mal configurado
• No se configuró x-default
• O x-default apunta al idioma incorrecto

3. El sitemap no incluye todos los idiomas
• Solo se enviaron algunas versiones
• El motor no puede descubrir todas las páginas

4. Formato de URL inconsistente
• Las URL de hreflang no siguen el mismo patrón
• Provoca errores de configuración

5. Problemas de contenido duplicado
• hreflang mal configurado
• El motor trata las versiones como duplicados

Solución:
• Configurar etiquetas hreflang
• Incluir todas las versiones de idioma
• Usar el formato de URL correcto
• Enviar un sitemap completo

Recomendación: sigue las buenas prácticas de este artículo para evitar estos errores.

19 min de lectura · Publicado el: 25 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog