链接替代语言版本 (hreflang)

告知搜索引擎您的本地化页面

问题

一个应用程序在 /en/page/fr/page 提供了相同的内容。搜索引擎将这些视为两个独立的、相互竞争的页面。如果没有机制将它们链接起来,搜索排名会被分散,法国的用户可能会在搜索结果中看到英文页面,而不是法文页面。

解决方案

在 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'; // 你的生产环境 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;

  // 创建语言替代项
  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}`,
      },
    },
  };
}

// 其余的布局组件
export default function RootLayout({ children, params }: Props) {
  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  );
}

此代码生成一个 languages 对象,将每个语言环境映射到其绝对基础 URL(例如,en: 'https://www.example.com/en')。它还为当前页面设置了一个 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'; // 此页面的路径

  // 创建语言替代项
  const alternatesMap = locales.reduce((acc, locale) => {
    acc[locale] = `${siteBaseUrl}/${locale}${path}`;
    return acc;
  }, {} as Record<string, string>);

  return {
    title: '关于我们', // 添加您的翻译标题
    alternates: {
      canonical: `${siteBaseUrl}/${lang}${path}`,
      languages: {
        ...alternatesMap,
        'x-default': `${siteBaseUrl}/${defaultLocale}${path}`,
      },
    },
  };
}

export default function AboutPage() {
  return <div>关于页面内容</div>;
}