Linking alternative language versions (hreflang)

Telling search engines about your localized pages

Problem

An application has identical content available at /en/page and /fr/page. Search engines see these as two separate, competing pages. Without a mechanism to link them, search rankings are split, and users in France may be shown the English page in search results instead of the French one.

Solution

Use the alternates property within the Next.js generateMetadata function. By providing a list of all available languages for a given page, Next.js will automatically generate <link rel="alternate" hreflang="..." /> tags in the document <head>, signaling the relationship between these pages to search engines.

Steps

1. Define your site's base URL

hreflang tags require absolute, not relative, URLs. Store your site's canonical base URL in a configuration file.

// i18n-config.ts

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

2. Add alternates to generateMetadata

In your app/[lang]/layout.tsx (to apply to all pages) or a specific page file, export a generateMetadata function.

// 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>
  );
}

This code generates a languages object that maps each locale to its absolute base URL (e.g., en: 'https://www.example.com/en'). It also sets a canonical URL for the current page and an x-default URL, which tells search engines which version to show for users in unspecified languages.

3. Handle alternates on nested pages

For nested pages like /about, you must ensure the metadata function includes the full path.

// 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>;
}