前端开发

Next.js 15 多语言站点实践:路由、SEO 与交付细节

围绕 App Router 的多语言站点实现,这篇文章整理了路由组织、翻译文件管理、SEO 配置和静态生成的基本做法。

张伟
2024/12/15
8 分钟阅读
Next.jsi18nSEOApp Router

Next.js 15 多语言站点实践:路由、SEO 与交付细节

多语言站点通常难的不是翻译几段文案,而是让路由、SEO、内容组织和性能细节配合好。Next.js 15 的 App Router 把这些基础能力组织得比较清楚。

项目初始化

先创建一个新的 Next.js 15 项目:

npx create-next-app@latest my-multilingual-site
cd my-multilingual-site
npm install next-intl

配置国际化

1. 创建语言配置文件

创建 src/lib/i18n.ts:

export const locales = ['zh', 'en', 'fr', 'de'] as const;
export type Locale = typeof locales[number];

export const defaultLocale: Locale = 'zh';

export const localeNames: Record<Locale, string> = {
  zh: '中文',
  en: 'English',
  fr: 'Français',
  de: 'Deutsch'
};

2. 配置中间件

创建 middleware.ts:

import { createMiddleware } from 'next-intl/middleware';
import { locales, defaultLocale } from './src/lib/i18n';

export default createMiddleware({
  locales,
  defaultLocale,
  localePrefix: 'always'
});

export const config = {
  matcher: [
    '/((?!api|_next|_vercel|.*\..*).*)',
    '/([\w-]+)?/users/(.+)'
  ]
};

路由结构设计

用 App Router 的动态路由组织页面:

src/app/
├── [locale]/
│   ├── layout.tsx
│   ├── page.tsx
│   ├── about/
│   │   └── page.tsx
│   ├── blog/
│   │   ├── page.tsx
│   │   └── [slug]/
│   │       └── page.tsx
│   └── contact/
│       └── page.tsx
├── globals.css
└── layout.tsx

翻译文件管理

创建翻译文件

messages/zh.json:

{
  "navigation": {
    "home": "首页",
    "about": "关于我们",
    "blog": "博客",
    "contact": "联系我们"
  },
  "home": {
    "title": "欢迎来到我们的网站",
    "description": "这是一个多语言网站示例"
  }
}

messages/en.json:

{
  "navigation": {
    "home": "Home",
    "about": "About",
    "blog": "Blog",
    "contact": "Contact"
  },
  "home": {
    "title": "Welcome to Our Website",
    "description": "This is a multilingual website example"
  }
}

组件国际化

创建导航组件

'use client';

import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import Link from 'next/link';

export default function Navigation() {
  const t = useTranslations('navigation');
  const params = useParams();
  const locale = params.locale as string;

  const navItems = [
    { key: 'home', href: `/${locale}` },
    { key: 'about', href: `/${locale}/about` },
    { key: 'blog', href: `/${locale}/blog` },
    { key: 'contact', href: `/${locale}/contact` }
  ];

  return (
    <nav className="flex space-x-6">
      {navItems.map(item => (
        <Link
          key={item.key}
          href={item.href}
          className="hover:text-blue-600 transition-colors"
        >
          {t(item.key)}
        </Link>
      ))}
    </nav>
  );
}

SEO 优化

多语言 SEO 配置

import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';

interface Props {
  params: { locale: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const t = await getTranslations({ locale: params.locale, namespace: 'metadata' });

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: `https://example.com/${params.locale}`,
      languages: {
        'zh': 'https://example.com/zh',
        'en': 'https://example.com/en',
        'fr': 'https://example.com/fr',
        'de': 'https://example.com/de'
      }
    },
    openGraph: {
      title: t('title'),
      description: t('description'),
      locale: params.locale,
      alternateLocale: ['zh', 'en', 'fr', 'de'].filter(l => l !== params.locale)
    }
  };
}

性能优化

1. 动态导入翻译文件

async function getMessages(locale: string) {
  try {
    return (await import(`../messages/${locale}.json`)).default;
  } catch (error) {
    return (await import('../messages/zh.json')).default;
  }
}

2. 静态生成优化

export function generateStaticParams() {
  return locales.map((locale) => ({ locale }));
}

语言切换器

'use client';

import { useParams, usePathname } from 'next/navigation';
import Link from 'next/link';
import { locales, localeNames } from '@/lib/i18n';

export default function LanguageSwitcher() {
  const params = useParams();
  const pathname = usePathname();
  const currentLocale = params.locale as string;

  const getLocalizedPath = (locale: string) => {
    const segments = pathname.split('/');
    segments[1] = locale;
    return segments.join('/');
  };

  return (
    <div className="relative">
      <select
        value={currentLocale}
        onChange={(e) => {
          const newPath = getLocalizedPath(e.target.value);
          window.location.href = newPath;
        }}
        className="bg-white border border-gray-300 rounded px-3 py-1"
      >
        {locales.map(locale => (
          <option key={locale} value={locale}>
            {localeNames[locale]}
          </option>
        ))}
      </select>
    </div>
  );
}

实施建议

1. 翻译键命名规范

  • 用嵌套结构组织翻译键
  • 键名保持简洁,并说明对应内容
  • 命名约定保持一致

2. 内容管理

  • 可以考虑用 CMS 管理多语言内容
  • 建立翻译工作流程
  • 定期审查、更新翻译

3. 测试策略

// 测试多语言路由
describe('Multilingual Routing', () => {
  test('should redirect to default locale', () => {
    // 测试逻辑
  });

  test('should preserve locale in navigation', () => {
    // 测试逻辑
  });
});

总结

Next.js 15 把多语言站点最关键的几层分得更清楚了:路由、翻译文件、metadata 和静态生成各有稳定的位置。只要先定好结构,后续维护就会轻松很多。

关键要点:

  • 用中间件处理语言检测和重定向
  • 合理组织翻译文件和路由结构
  • 重视 SEO 优化和性能提升
  • 建立完善的测试、维护流程

如果已经确定要做多语言站点,先把路由、翻译文件和 metadata 三层结构定稳,之后接入 CMS 和动态翻译时,才不容易返工。

想聊聊技术?

对某篇内容有想法,或想交流做产品的经验,随时来找我。