链接其他语言版本(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'; // 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>
  );
}

此代码会生成一个 languages 对象,将每个 locale 映射到其绝对基础 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'; // 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>;
}