Vinculación de versiones de idiomas alternativos (hreflang)

Informar a los motores de búsqueda sobre tus páginas localizadas

Problema

Una aplicación tiene contenido idéntico disponible en /en/page y /fr/page. Los motores de búsqueda ven estas como dos páginas separadas y competidoras. Sin un mecanismo para vincularlas, las clasificaciones de búsqueda se dividen, y los usuarios en Francia pueden ver la página en inglés en los resultados de búsqueda en lugar de la francesa.

Solución

Utiliza la propiedad alternates dentro de la función generateMetadata de Next.js. Al proporcionar una lista de todos los idiomas disponibles para una página determinada, Next.js generará automáticamente etiquetas <link rel="alternate" hreflang="..." /> en el <head> del documento, señalando la relación entre estas páginas a los motores de búsqueda.

Pasos

1. Define la URL base de tu sitio

Las etiquetas hreflang requieren URLs absolutas, no relativas. Almacena la URL base canónica de tu sitio en un archivo de configuración.

// i18n-config.ts

export const locales = ['en', 'es', 'fr'];
export const defaultLocale = 'en';
export const siteBaseUrl = 'https://www.example.com'; // Your production URL

2. Añade alternates a generateMetadata

En tu app/[lang]/layout.tsx (para aplicar a todas las páginas) o en un archivo de página específico, exporta una función generateMetadata.

// app/[lang]/layout.tsx
import { locales, siteBaseUrl } from '@/i18n-config';
import type { Metadata } from 'next';

type Props = {
  params: { lang: string };
  children: React.ReactNode;
};

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

  // Create language alternates
  const alternatesMap = locales.reduce((acc, locale) => {
    acc[locale] = `${siteBaseUrl}/${locale}`;
    return acc;
  }, {} as Record<string, string>);

  return {
    alternates: {
      canonical: `${siteBaseUrl}/${lang}`,
      languages: {
        ...alternatesMap,
        'x-default': `${siteBaseUrl}/${defaultLocale}`,
      },
    },
  };
}

// Rest of your layout component
export default function RootLayout({ children, params }: Props) {
  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  );
}

Este código genera un objeto languages que mapea cada locale a su URL base absoluta (por ejemplo, en: 'https://www.example.com/en'). También establece una URL canonical para la página actual y una URL x-default, que indica a los motores de búsqueda qué versión mostrar para usuarios en idiomas no especificados.

3. Gestiona alternates en páginas anidadas

Para páginas anidadas como /about, debes asegurarte de que la función de metadatos incluya la ruta completa.

// app/[lang]/about/page.tsx
import { locales, siteBaseUrl, defaultLocale } from '@/i18n-config';
import type { Metadata } from 'next';

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

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang } = params;
  const path = '/about'; // The path for this page

  // Create language alternates
  const alternatesMap = locales.reduce((acc, locale) => {
    acc[locale] = `${siteBaseUrl}/${locale}${path}`;
    return acc;
  }, {} as Record<string, string>);

  return {
    title: 'About Us', // Add your translated title
    alternates: {
      canonical: `${siteBaseUrl}/${lang}${path}`,
      languages: {
        ...alternatesMap,
        'x-default': `${siteBaseUrl}/${defaultLocale}${path}`,
      },
    },
  };
}

export default function AboutPage() {
  return <div>About page content</div>;
}