代替言語バージョンのリンク(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. generateMetadataにalternatesを追加する
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>;
}