Next.js 動態路由與 Catch-all

當你無法事先知道確定的路由路徑(例如部落格文章的 ID 或產品清單)時,你需要使用 動態路由 (Dynamic Routes)。Next.js 讓你能透過資料夾命名的慣例,輕鬆定義動態參數。

基礎動態路由 [slug]

使用方括號 [] 包裹資料夾名稱,即可建立動態分段。

範例:

app/blog/[slug]/page.tsx 會對應到 /blog/hello-world/blog/nextjs-tutorial

在頁面元件中,你可以透過 params 屬性取得該參數:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>文章內容:{params.slug}</h1>;
}

多重動態路由

你可以在同一個路徑中組合多個動態分段。

範例:

app/shop/[category]/[item]/page.tsx 會對應到 /shop/shoes/nike-air-maxparams 會是 { category: 'shoes', item: 'nike-air-max' }

Catch-all 路由 [...slug]

如果你想匹配後續所有的層級,可以在方括號內加上三個點 ...

範例:

app/docs/[...slug]/page.tsx 會對應到:

  • /docs/introduction
  • /docs/setup/macos
  • /docs/api/auth/providers

此時 params.slug 會是一個字串陣列['api', 'auth', 'providers']

Optional Catch-all [[...slug]]

一般的 Catch-all 路由必須至少包含一個子路徑。如果你希望連根路徑也能匹配,可以使用雙重方括號 [[]]

比較:

  • [...slug]:匹配 /docs/a,但不匹配 /docs
  • [[...slug]]:匹配 /docs/a,也匹配 /docs

這在建立複雜的文檔導航或 CMS 系統時非常有用。

程式碼範例:

當使用 Optional Catch-all 時,若使用者訪問的是根路徑(如 /docs),params.slug 的值會是 undefined(或空陣列,視 Next.js 版本與處理方式而定,但在 Server Component 中通常需作空值檢查)。

// app/docs/[[...slug]]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
  // 如果訪問 /docs,params.slug 為 undefined
  // 如果訪問 /docs/intro,params.slug 為 ['intro']

  const slug = params.slug || [];

  if (slug.length === 0) {
    return <h1>文檔首頁</h1>;
  }

  return <h1>正在閱讀:{slug.join(' / ')}</h1>;
}
路由路徑網址 URLparams 結果
app/docs/[[...slug]]/page.tsx/docs{} (slug 為 undefined)
app/docs/[[...slug]]/page.tsx/docs/intro{ slug: ['intro'] }
app/docs/[[...slug]]/page.tsx/docs/intro/setup{ slug: ['intro', 'setup'] }

生成靜態參數 (generateStaticParams)

如果你使用動態路由,但希望在編譯時就先生成好靜態頁面(Statoc Site Generation, SSG),你可以導出 generateStaticParams 函式。這能極大地提升效能,因為頁面在要求時就已經是準備好的 HTML。

函式用途與語法

generateStaticParams 必須回傳一個物件陣列,每個物件代表一個路由的參數組合。

// app/blog/[slug]/page.tsx

// 1. 定義要生成的靜態路徑列表
export async function generateStaticParams() {
  // 模擬從資料庫或 API 取得所有文章
  const posts = await fetch('https://api.example.com/posts').then((res) => res.json());

  // 回傳物件陣列,每個物件的 key 必須對應動態路由的參數名稱 (如 slug)
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

// 2. 頁面組件
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>文章 ID: {params.slug}</h1>;
}

控制未生成的路徑 (dynamicParams)

預設情況下,如果使用者訪問了一個不在 generateStaticParams 回傳列表中的路徑,Next.js 會嘗試在伺服器端即時生成該頁面 (On-demand Rendering)。

你可以透過導出 dynamicParams 變數來控制這個行為:

// true (預設值): 當訪問未生成的路徑時,嘗試即時生成頁面
// false: 當訪問未生成的路徑時,直接回傳 404
export const dynamicParams = false;

export async function generateStaticParams() {
  return [{ slug: 'a' }, { slug: 'b' }];
}
  • true (預設):適合部落格文章,新文章發布後不需要重新部署即可被訪問。
  • false:適合內容固定的網站,確保只有預定義的路徑可以被訪問。
Next.js 建議在處理大量動態路由時,盡量利用 generateStaticParams 來優化核心關鍵指標 (Core Web Vitals),並根據需求設定 dynamicParams 以平衡構建時間與使用者體驗。