Atrás

Cómo optimizar el SEO para una app Next.js multilingüe

General Translation avatarGeneral Translation
guideseointernationalizationnextjsi18nroutingmetadata

Por qué el SEO multilingüe requiere una atención especial

La mayoría de los desarrolladores que añaden i18n a su aplicación Next.js se centran en traducir la UI y dan el trabajo por terminado. Pero si los motores de búsqueda no pueden encontrar, indexar y asociar correctamente tus versiones en distintos idiomas, todo ese trabajo de traducción es invisible.

Un sitio multilingüe sin una configuración SEO adecuada presenta varios problemas:

  • Es posible que Google indexe solo una versión de idioma e ignore el resto
  • Los usuarios que buscan en español terminan viendo la página en inglés
  • Penalizaciones por contenido duplicado porque Google ve /en/about y /fr/about como la misma página
  • Idioma incorrecto mostrado en los fragmentos de resultados de búsqueda

La buena noticia: hacer bien el SEO multilingüe en Next.js no es complicado. Hay cinco aspectos que debes configurar correctamente, y esta guía los cubre todos utilizando gt-next.


1. Enrutamiento de URL basado en locale

La base del SEO multilingüe es tener URL distintas para cada idioma. Los motores de búsqueda necesitan URL separadas y rastreables para indexar cada versión de idioma de forma independiente.

Esto significa locale en la URL — no cookies, no parámetros de consulta, no solo detección mediante Accept-Language.

✅ generaltranslation.com/en/about
✅ generaltranslation.com/fr/about
✅ generaltranslation.com/es/about

❌ generaltranslation.com/about?lang=fr
❌ generaltranslation.com/about (con el locale en una cookie)

Configurar el enrutamiento por locales con gt-next

Primero, coloca tus páginas dentro de un segmento dinámico [locale]:

app/
└── [locale]/
    ├── layout.tsx
    ├── page.tsx
    └── about/
        └── page.tsx

Luego, crea un middleware en la raíz del proyecto (proxy.ts para Next.js 16+ o middleware.ts para Next.js 15 y anteriores):

import { createNextMiddleware } from 'gt-next/middleware';

export default createNextMiddleware();

export const config = {
  matcher: ['/((?!api|static|.*\\..*|_next).*)'],
};

Esto genera automáticamente URL con prefijo de locale. De forma predeterminada, la locale predeterminada (por ejemplo, inglés) no lleva un prefijo — /about se mantiene sin cambios, mientras que las personas usuarias en español ven /es/about y las personas usuarias en francés ven /fr/about.


2. Configuración del atributo lang de HTML

El atributo lang en la etiqueta <html> indica a los navegadores y a los motores de búsqueda en qué idioma está la página. Es una de las cosas más simples y con mayor impacto que puedes hacer para la accesibilidad y el SEO.

Sin él, los lectores de pantalla adivinan el idioma (a menudo de forma incorrecta), y los motores de búsqueda tienen menos confianza en su clasificación del idioma.

gt-next proporciona el hook useLocale que simplifica esto en tu layout raíz:

import { useLocale, GTProvider } from 'gt-next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const locale = useLocale();

  return (
    <html lang={locale}>
      <body>
        <GTProvider>
          {children}
        </GTProvider>
      </body>
    </html>
  );
}

useLocale devuelve el código de configuración regional BCP 47 (por ejemplo, en-US, ar, zh-Hans).


3. URLs canónicas

Las etiquetas canónicas indican a los motores de búsqueda qué URL es la versión «principal» de una página. En sitios multilingües, cada versión en un idioma debe señalarse a sí misma como canónica:

<!-- En /fr/about -->
<link rel="canonical" href="https://example.com/fr/about" />

Esto evita que los motores de búsqueda consideren tu página en francés como un duplicado de tu página en inglés.

En Next.js, configuras las etiquetas canónicas mediante la API de metadatos. Combínala con getLocale de gt-next para generar la URL canónica correcta para cada locale:

import { getLocale } from 'gt-next/server';

const BASE_URL = 'https://example.com';

export async function generateMetadata() {
  const locale = await getLocale();

  return {
    alternates: {
      canonical: `${BASE_URL}/${locale}/about`,
    },
  };
}

export default function AboutPage() {
  return <h1>About Us</h1>;
}

Para el locale predeterminado sin prefijo, ajusta el ejemplo en consecuencia:

import { getLocale, getDefaultLocale } from 'gt-next/server';

export async function generateMetadata() {
  const locale = await getLocale();
  const defaultLocale = getDefaultLocale();
  const path = '/about';

  const prefix = locale === defaultLocale ? '' : `/${locale}`;

  return {
    alternates: {
      canonical: `${BASE_URL}${prefix}${path}`,
    },
  };
}

4. Etiquetas hreflang

Las etiquetas hreflang son la señal de SEO multilingüe más importante. Indican a los motores de búsqueda: "esta página está disponible en estos otros idiomas, y aquí están las URL."

Sin hreflang, Google tiene que adivinar qué versión de idioma mostrar en los resultados de búsqueda, y a menudo se equivoca.

<link rel="alternate" hreflang="en" href="https://example.com/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
<link rel="alternate" hreflang="es" href="https://example.com/es/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />

La etiqueta x-default indica a los motores de búsqueda qué URL mostrar cuando ninguno de los idiomas especificados coincide con el idioma del usuario.

En Next.js, puedes añadir hreflang mediante la propiedad alternates.languages de la API de metadatos. Aquí tienes una función auxiliar reutilizable que funciona con gt-next:

import { getLocale, getDefaultLocale } from 'gt-next/server';

const BASE_URL = 'https://example.com';
const SUPPORTED_LOCALES = ['en', 'fr', 'es'];

export async function getI18NMetadata(path: string) {
  const locale = await getLocale();
  const defaultLocale = getDefaultLocale();

  const getUrl = (loc: string) => {
    const prefix = loc === defaultLocale ? '' : `/${loc}`;
    return `${BASE_URL}${prefix}${path}`;
  };

  const languages: Record<string, string> = {};
  for (const loc of SUPPORTED_LOCALES) {
    languages[loc] = getUrl(loc);
  }
  languages['x-default'] = getUrl(defaultLocale);

  return {
    alternates: {
      canonical: getUrl(locale),
      languages,
    },
  };
}

Después, utilízalo en cualquier página:

import { getI18NMetadata } from '@/lib/i18n-metadata';

export async function generateMetadata() {
  return await getI18NMetadata('/about');
}

Esto genera tanto la etiqueta canónica como todas las etiquetas hreflang en una única llamada.


5. Metadatos traducidos

Los motores de búsqueda muestran el título y la descripción de tu página en los resultados. Si el título y la descripción de una página en francés están en inglés, es menos probable que los usuarios hagan clic, y Google puede posicionar el resultado más abajo.

Usa la función getGT de gt-next para traducir las cadenas de metadatos:

import { getGT } from 'gt-next/server';
import { getI18NMetadata } from '@/lib/i18n-metadata';

export async function generateMetadata() {
  const t = await getGT();
  const i18nMeta = await getI18NMetadata('/about');

  return {
    title: t('Acerca de nosotros'),
    description: t('Conoce nuestra misión y nuestro equipo.'),
    ...i18nMeta,
  };
}

Esto te proporciona títulos y descripciones localizados en los resultados de búsqueda, lo que mejora significativamente el porcentaje de clics para consultas en otros idiomas distintos del inglés.


6. Sitemaps multilingües

Un sitemap ayuda a los motores de búsqueda a descubrir todas tus páginas, incluidas todas sus versiones en cada idioma. En sitios multilingües, también deberías incluir anotaciones hreflang en tu sitemap.

Next.js admite sitemaps programáticos mediante un archivo sitemap.ts:

import { MetadataRoute } from 'next';

const BASE_URL = 'https://example.com';
const LOCALES = ['en', 'fr', 'es'];
const DEFAULT_LOCALE = 'en';
const PAGES = ['/', '/about', '/blog', '/contact'];

export default function sitemap(): MetadataRoute.Sitemap {
  return PAGES.flatMap((path) => {
    const getUrl = (locale: string) => {
      const prefix = locale === DEFAULT_LOCALE ? '' : `/${locale}`;
      return `${BASE_URL}${prefix}${path === '/' ? '' : path}`;
    };

    const languages: Record<string, string> = {};
    for (const locale of LOCALES) {
      languages[locale] = getUrl(locale);
    }

    return LOCALES.map((locale) => ({
      url: getUrl(locale),
      lastModified: new Date(),
      alternates: { languages },
    }));
  });
}

Esto genera un sitemap con una entrada por página y por idioma, y cada entrada incluye anotaciones hreflang que apuntan a todas las versiones en todos los idiomas.


Lista de verificación

Aquí tienes un resumen rápido de todo lo que hemos visto:

Requisito de SEOImplementación
Locale en la URLcreateNextMiddleware() con el segmento dinámico [locale]
Atributo HTML languseLocale() en el layout raíz
URLs canónicasgetLocale() + Next.js metadata API alternates.canonical
Etiquetas hreflangNext.js metadata API alternates.languages con todos los locales admitidos
Metadatos traducidosgetGT() para títulos y descripciones de página
Sitemap multilingüesitemap.ts con entradas por locale y alternates hreflang

Próximos pasos