Next.js Metadata 與 SEO
Next.js 對搜尋引擎最佳化 (SEO) 提供了開箱即用的強大支援。透過最新的 Metadata API,你可以輕鬆地為每個頁面定義標題、描述、關鍵字以及社群分享預覽圖(Open Graph)。
此外,Next.js 也提供了專門的檔案慣例 (File Conventions) 來自動生成 sitemap.xml 與 robots.txt,這是現代 SEO 不可或缺的兩大支柱。
Metadata 設定
靜態 Metadata
對於內容固定的頁面(如首頁、關於我們),你只需要導出一個名為 metadata 的靜態物件。
// app/page.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Next.js 學習手冊',
description: '從零開始掌握現代網頁開發技術',
};
export default function Page() {}
動態 Metadata
如果你的頁面內容是根據 URL 參數動態決定的(例如部落格文章詳情頁),你需要導出 generateMetadata 異步函式。
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
type Props = {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// 從 API 或資料庫獲取文章數據
const slug = params.slug;
const post = await fetch(`https://api.example.com/posts/${slug}`).then((res) => res.json());
// 獲取父層級的 metadata (例如繼承的 Open Graph圖片)
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.summary,
openGraph: {
images: [post.coverImage, ...previousImages],
},
};
}
Metadata 繼承與模板
Next.js 的 Metadata 具有層級關係:Page 層級會覆蓋 Layout 層級。
為了保持全站標題格式一致(例如每個頁面都掛上 | 我的品牌),可以使用 title.template:
// app/layout.tsx (Root Layout)
export const metadata = {
title: {
default: '我的超棒網站', // 當子頁面沒設定 title 時顯示
template: '%s | 我的超棒網站', // 當子頁面有設定 title 時,填入 %s
},
};
// app/about/page.tsx
export const metadata = {
title: '關於我們', // 最終顯示: "關於我們 | 我的超棒網站"
};
Sitemap (網站地圖)
Sitemap 是告訴搜尋引擎你有哪些頁面需要被索引的重要檔案。Next.js 支援透過 app/sitemap.ts 自動生成。
基礎靜態 Sitemap
如果你的網站頁面不多且相對固定,可以直接回傳一個陣列。
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://acme.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://acme.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://acme.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
];
}
動態 Sitemap
對於部落格、電商網站,我們需要從資料庫撈取所有文章網址來動態生成。
// app/sitemap.ts
import { MetadataRoute } from 'next';
// 模擬從資料庫取得文章列表
async function getBlogPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getBlogPosts();
// 1. 定義靜態頁面
const staticRoutes = [
{
url: 'https://acme.com',
lastModified: new Date(),
},
{
url: 'https://acme.com/about',
lastModified: new Date(),
},
];
// 2. 將文章列表轉換為 Sitemap 格式
const dynamicRoutes = posts.map((post) => ({
url: `https://acme.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.7,
}));
// 3. 合併回傳
return [...staticRoutes, ...dynamicRoutes];
}
大型網站 Sitemap (分頁處理)
如果你的網址超過 50,000 個(Google 的單一 Sitemap 上限),你需要使用 generateSitemaps 來產生多個 Sitemap 檔案(例如 sitemap/1.xml, sitemap/2.xml)。
// app/sitemap.ts
import { MetadataRoute } from 'next';
// 假設我們有 10 萬篇文章,每頁 5 萬筆,共 2 頁
// ID 會是 0 和 1
export async function generateSitemaps() {
// 回傳這網站共有幾個 sitemap ID
return [{ id: 0 }, { id: 1 }];
}
export default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {
// 根據 ID 去資料庫撈取對應範圍的文章 (Limit/Offset)
const start = id * 50000;
const posts = await getPostsFromDB(start, 50000);
return posts.map((post) => ({
url: `https://acme.com/blog/${post.id}`,
lastModified: post.date,
}));
}
Robots.txt
robots.txt 用來告訴搜尋引擎爬蟲哪些頁面可以抓取,哪些禁止進入。透過 app/robots.ts 可以動態生成。
常用情境範例
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://acme.com';
return {
rules: {
userAgent: '*', // 針對所有爬蟲
allow: '/', // 允許爬取所有與頁面
disallow: ['/admin/', '/private/'], // 禁止爬取代與頁面
},
sitemap: `${baseUrl}/sitemap.xml`, // 重要:告訴爬蟲 Sitemap 在哪
};
}
區分正式站與測試站
你通常不希望 Google 索引你的測試環境 (Staging)。可以透過環境變數來動態調整規則。
// app/robots.ts
export default function robots(): MetadataRoute.Robots {
// 判斷是否為正式環境
const isProduction = process.env.VERCEL_ENV === 'production';
if (!isProduction) {
// 測試環境:完全禁止爬蟲
return {
rules: {
userAgent: '*',
disallow: '/',
},
};
}
// 正式環境:正常開放
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/api/',
},
sitemap: 'https://acme.com/sitemap.xml',
};
}
Open Graph (OG) 圖片
社群分享時顯示的預覽圖對點擊率影響巨大。Next.js 提供了兩種方式:
- 靜態檔案:直接在路由資料夾放
opengraph-image.png或twitter-image.png。 - 動態生成:使用
ImageResponseAPI。
使用 ImageResponse 動態生成
這非常適合「文章標題不同,圖片文字就不同」的需求。建立 opengraph-image.tsx:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export default async function Image({ params }: { params: { slug: string } }) {
const post = await fetchPost(params.slug);
return new ImageResponse(
<div
style={{
fontSize: 48,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{post.title}
</div>,
{
width: 1200,
height: 630,
}
);
}
小結
- Metadata:務必設定
title,description與 Open Graph,這對 SEO 是基本功。 - Sitemap:使用
app/sitemap.ts自動對映你的路由,大型網站需記得做分頁。 - Robots.txt:使用
app/robots.ts來防止測試站被索引,並明確指出 Sitemap 位置。 - 自動化:Next.js 的這些 API 都是基於程式碼生成的,這意味著你的 SEO 設定會隨著資料庫內容自動更新,無需人工維護。