Chuyển đến nội dung chính

第 15 課:國際化與 SEO

Next.js i18n 路由,next-intl。動態元資料 API。 OpenGraph、Twitter 卡。 JSON-LD 結構化資料。網站地圖、robots.txt 產生。

💻 程式設計 — 第 15 課 第 15 課:國際化與 SEO

React 和 Next.js:從基礎到高級

第 4 部分:Next.js 高級

亞洲開發網

1.國際化路由

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: ['/', '/(vi|en)/:path*'],
};
// i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['vi', 'en'],
  defaultLocale: 'vi',
});
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;
  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale;
  }

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
  };
});

2. 翻譯文件

// messages/vi.json
{
  "HomePage": {
    "title": "Chào mừng đến với website",
    "description": "Nền tảng học lập trình"
  },
  "Navigation": {
    "home": "Trang chủ",
    "blog": "Blog",
    "about": "Giới thiệu"
  }
}
// Sử dụng trong component
import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('HomePage');
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

3. 動態元資料API

import type { Metadata } from 'next';

// Static metadata
export const metadata: Metadata = {
  title: 'Trang chủ | xDev',
  description: 'Nền tảng học lập trình',
  openGraph: {
    title: 'xDev',
    description: 'Nền tảng học lập trình',
    images: ['/images/og-image.jpg'],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'xDev',
    description: 'Nền tảng học lập trình',
    images: ['/images/og-image.jpg'],
  },
};

// Dynamic metadata
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [post.image],
      type: 'article',
      publishedTime: post.createdAt,
      authors: [post.author.name],
    },
  };
}

4. JSON-LD結構化數據

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.description,
    image: post.image,
    datePublished: post.createdAt,
    author: {
      '@type': 'Person',
      name: post.author.name,
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
      </article>
    </>
  );
}

5. 網站地圖和機器人

// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await db.post.findMany({
    where: { published: true },
    select: { slug: true, updatedAt: true },
  });

  return [
    { url: 'https://blog.xdev.asia', lastModified: new Date(), priority: 1 },
    { url: 'https://blog.xdev.asia/blog', lastModified: new Date(), priority: 0.8 },
    ...posts.map(post => ({
      url: `https://blog.xdev.asia/blog/${post.slug}`,
      lastModified: post.updatedAt,
      priority: 0.6,
    })),
  ];
}

// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      { userAgent: '*', allow: '/', disallow: '/api/' },
    ],
    sitemap: 'https://blog.xdev.asia/sitemap.xml',
  };
}

下一篇: 文件上傳和圖像優化 — 上傳檔案、下一個/圖像、回應影像。