代替言語バージョンのリンク(hreflang)

ローカライズされたページを検索エンジンに伝える

問題

アプリケーションが/en/page/fr/pageで同一のコンテンツを提供している場合、検索エンジンはこれらを2つの別々の競合するページとして認識します。これらをリンクする仕組みがないと、検索ランキングが分散し、フランスのユーザーに対して検索結果でフランス語ページではなく英語ページが表示される可能性があります。

解決策

Next.jsのgenerateMetadata関数内でalternatesプロパティを使用します。特定のページで利用可能なすべての言語のリストを提供することで、Next.jsは自動的にドキュメントの<head>内に<link rel="alternate" hreflang="..." />タグを生成し、これらのページ間の関係を検索エンジンに通知します。

手順

1. サイトのベースURLを定義する

hreflangタグには相対URLではなく絶対URLが必要です。サイトの正規ベースURLを設定ファイルに保存します。

// i18n-config.ts

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

2. generateMetadataalternatesを追加する

app/[lang]/layout.tsx(すべてのページに適用する場合)または特定のページファイルで、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>
  );
}

このコードは、各ロケールをその絶対ベースURL(例:en: 'https://www.example.com/en')にマッピングするlanguagesオブジェクトを生成します。また、現在のページのcanonical URLと、指定されていない言語のユーザーに対してどのバージョンを表示するかを検索エンジンに伝えるx-default URLも設定します。

3. ネストされたページでalternatesを処理する

/aboutのようなネストされたページの場合、メタデータ関数に完全なパスを含める必要があります。

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