nextjs-app-router-fundamentals

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.js App Router Fundamentals

Next.js App Router 基础

Overview

概述

Provide comprehensive guidance for Next.js App Router (Next.js 13+), covering migration from Pages Router, file-based routing conventions, layouts, metadata handling, and modern Next.js patterns.
提供Next.js App Router(Next.js 13+)的全面指南,涵盖从Pages Router的迁移、基于文件的路由约定、布局、元数据处理以及现代Next.js模式。

TypeScript: NEVER Use
any
Type

TypeScript:切勿使用
any
类型

CRITICAL RULE: This codebase has
@typescript-eslint/no-explicit-any
enabled. Using
any
will cause build failures.
❌ WRONG:
typescript
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ CORRECT:
typescript
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
重要规则: 此代码库已启用
@typescript-eslint/no-explicit-any
规则。使用
any
类型会导致构建失败。
❌ 错误示例:
typescript
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ 正确示例:
typescript
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];

Common Next.js Type Patterns

常见Next.js类型模式

typescript
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }

// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }

// Server actions
async function myAction(formData: FormData) { ... }
typescript
// 页面属性
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }

// 表单事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }

// 服务器操作
async function myAction(formData: FormData) { ... }

When to Use This Skill

何时使用本指南

Use this skill when:
  • Migrating from Pages Router (
    pages/
    directory) to App Router (
    app/
    directory)
  • Creating Next.js 13+ applications from scratch
  • Working with layouts, templates, and nested routing
  • Implementing metadata and SEO optimizations
  • Building with App Router routing conventions
  • Handling route groups, parallel routes, or intercepting routes basics
在以下场景使用本指南:
  • 从Pages Router(
    pages/
    目录)迁移到App Router(
    app/
    目录)
  • 从零开始创建Next.js 13+应用
  • 处理布局、模板和嵌套路由
  • 实现元数据和SEO优化
  • 遵循App Router路由约定进行开发
  • 处理路由组、并行路由或基础拦截路由

Core Concepts

核心概念

App Router vs Pages Router

App Router vs Pages Router

Pages Router (Legacy - Next.js 12 and earlier):
pages/
├── index.tsx              # Route: /
├── about.tsx              # Route: /about
├── _app.tsx               # Custom App component
├── _document.tsx          # Custom Document component
└── api/                   # API routes
    └── hello.ts           # API endpoint: /api/hello
App Router (Modern - Next.js 13+):
app/
├── layout.tsx             # Root layout (required)
├── page.tsx               # Route: /
├── about/                 # Route: /about
│   └── page.tsx
├── blog/
│   ├── layout.tsx         # Nested layout
│   └── [slug]/
│       └── page.tsx       # Dynamic route: /blog/:slug
└── api/                   # Route handlers
    └── hello/
        └── route.ts       # API endpoint: /api/hello
Pages Router(旧版 - Next.js 12及更早版本):
pages/
├── index.tsx              # 路由: /
├── about.tsx              # 路由: /about
├── _app.tsx               # 自定义App组件
├── _document.tsx          # 自定义Document组件
└── api/                   # API路由
    └── hello.ts           # API端点: /api/hello
App Router(新版 - Next.js 13+):
app/
├── layout.tsx             # 根布局(必填)
├── page.tsx               # 路由: /
├── about/                 # 路由: /about
│   └── page.tsx
├── blog/
│   ├── layout.tsx         # 嵌套布局
│   └── [slug]/
│       └── page.tsx       # 动态路由: /blog/:slug
└── api/                   # 路由处理器
    └── hello/
        └── route.ts       # API端点: /api/hello

File Conventions

文件约定

Special Files in App Router:
  • layout.tsx
    - Shared UI for a segment and its children (preserves state, doesn't re-render)
  • page.tsx
    - Unique UI for a route, makes route publicly accessible
  • loading.tsx
    - Loading UI with React Suspense
  • error.tsx
    - Error UI with Error Boundaries
  • not-found.tsx
    - 404 UI
  • template.tsx
    - Similar to layout but re-renders on navigation
  • route.ts
    - API endpoints (Route Handlers)
Colocation:
  • Components, tests, and other files can be colocated in
    app/
  • Only
    page.tsx
    and
    route.ts
    files create public routes
  • Other files (components, utils, tests) are NOT routable
App Router中的特殊文件:
  • layout.tsx
    - 为某个路由段及其子路由提供共享UI(保留状态,不会重新渲染)
  • page.tsx
    - 路由的专属UI,使路由可公开访问
  • loading.tsx
    - 基于React Suspense的加载UI
  • error.tsx
    - 基于Error Boundaries的错误UI
  • not-found.tsx
    - 404页面UI
  • template.tsx
    - 与布局类似,但在导航时会重新渲染
  • route.ts
    - API端点(路由处理器)
就近放置:
  • 组件、测试和其他文件可放置在
    app/
    目录中
  • 只有
    page.tsx
    route.ts
    文件会创建公开路由
  • 其他文件(组件、工具类、测试)不可路由

Migration Guide: Pages Router to App Router

迁移指南:从Pages Router到App Router

Step 1: Understand the Current Structure

步骤1:了解当前结构

Examine existing Pages Router setup:
  • Read
    pages/
    directory structure
  • Identify
    _app.tsx
    - handles global state, layouts, providers
  • Identify
    _document.tsx
    - customizes HTML structure
  • Note metadata usage (
    next/head
    ,
    <Head>
    component)
  • List all routes and dynamic segments
检查现有Pages Router设置:
  • 查看
    pages/
    目录结构
  • 找到
    _app.tsx
    - 处理全局状态、布局、提供者
  • 找到
    _document.tsx
    - 自定义HTML结构
  • 记录元数据的使用方式(
    next/head
    <Head>
    组件)
  • 列出所有路由和动态路由段

Step 2: Create Root Layout

步骤2:创建根布局

Create
app/layout.tsx
- REQUIRED for all App Router applications:
typescript
// app/layout.tsx
export const metadata = {
  title: 'My App',
  description: 'App description',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
Migration Notes:
  • Move
    _document.tsx
    HTML structure to
    layout.tsx
  • Move
    _app.tsx
    global providers/wrappers to
    layout.tsx
  • Convert
    <Head>
    metadata to
    metadata
    export
  • The root layout MUST include
    <html>
    and
    <body>
    tags
创建
app/layout.tsx
- 所有App Router应用都必填
typescript
// app/layout.tsx
export const metadata = {
  title: 'My App',
  description: 'App description',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
迁移注意事项:
  • _document.tsx
    中的HTML结构迁移到
    layout.tsx
  • _app.tsx
    中的全局提供者/包装器迁移到
    layout.tsx
  • <Head>
    元数据转换为导出的
    metadata
    对象
  • 根布局必须包含
    <html>
    <body>
    标签

Step 3: Migrate Pages to Routes

步骤3:迁移页面到路由

Simple Page Migration:
typescript
// Before: pages/index.tsx
import Head from 'next/head';

export default function Home() {
  return (
    <>
      <Head>
        <title>Home Page</title>
      </Head>
      <main>
        <h1>Welcome</h1>
      </main>
    </>
  );
}
typescript
// After: app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
    </main>
  );
}

// Metadata moved to layout.tsx or exported here
export const metadata = {
  title: 'Home Page',
};
Nested Route Migration:
typescript
// Before: pages/blog/[slug].tsx
export default function BlogPost() { ... }
typescript
// After: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
简单页面迁移:
typescript
// 之前: pages/index.tsx
import Head from 'next/head';

export default function Home() {
  return (
    <>
      <Head>
        <title>Home Page</title>
      </Head>
      <main>
        <h1>Welcome</h1>
      </main>
    </>
  );
}
typescript
// 之后: app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Welcome</h1>
    </main>
  );
}

// 元数据已迁移到layout.tsx或在此处导出
export const metadata = {
  title: 'Home Page',
};
嵌套路由迁移:
typescript
// 之前: pages/blog/[slug].tsx
export default function BlogPost() { ... }
typescript
// 之后: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }

Step 4: Update Navigation

步骤4:更新导航

Replace anchor tags with Next.js Link:
typescript
// Before (incorrect in App Router)
<a href="/about">About</a>

// After (correct)
import Link from 'next/link';
<Link href="/about">About</Link>
使用Next.js Link替换锚标签:
typescript
// 之前(在App Router中不正确)
<a href="/about">About</a>

// 之后(正确)
import Link from 'next/link';
<Link href="/about">About</Link>

Step 5: Clean Up Pages Directory

步骤5:清理Pages目录

After migration:
  • Remove all page files from
    pages/
    directory
  • Keep
    pages/api/
    if you're not migrating API routes yet
  • Remove
    _app.tsx
    and
    _document.tsx
    (functionality moved to layout)
  • Optionally delete empty
    pages/
    directory
迁移完成后:
  • 删除
    pages/
    目录中的所有页面文件
  • 如果尚未迁移API路由,可保留
    pages/api/
  • 删除
    _app.tsx
    _document.tsx
    (功能已迁移到layout)
  • 可选择删除空的
    pages/
    目录

Metadata Handling

元数据处理

Static Metadata

静态元数据

typescript
// app/page.tsx or app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
  keywords: ['nextjs', 'react'],
  openGraph: {
    title: 'My Page',
    description: 'Page description',
    images: ['/og-image.jpg'],
  },
};
typescript
// app/page.tsx 或 app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
  keywords: ['nextjs', 'react'],
  openGraph: {
    title: 'My Page',
    description: 'Page description',
    images: ['/og-image.jpg'],
  },
};

Dynamic Metadata

动态元数据

typescript
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
  };
}
typescript
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
  };
}

Layouts and Nesting

布局与嵌套

Creating Nested Layouts

创建嵌套布局

typescript
// app/layout.tsx - Root layout
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

// app/blog/layout.tsx - Blog layout
export default function BlogLayout({ children }) {
  return (
    <div>
      <BlogSidebar />
      <main>{children}</main>
    </div>
  );
}
Layout Behavior:
  • Layouts preserve state across navigation
  • Layouts don't re-render on route changes
  • Parent layouts wrap child layouts
  • Root layout is required and wraps entire app
typescript
// app/layout.tsx - 根布局
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}

// app/blog/layout.tsx - 博客布局
export default function BlogLayout({ children }) {
  return (
    <div>
      <BlogSidebar />
      <main>{children}</main>
    </div>
  );
}
布局行为:
  • 布局在导航时保留状态
  • 布局在路由变化时不会重新渲染
  • 父布局包裹子布局
  • 根布局是必填项,包裹整个应用

Routing Patterns

路由模式

Dynamic Routes

动态路由

typescript
// app/blog/[slug]/page.tsx
export default function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  return <article>Post: {params.slug}</article>;
}
typescript
// app/blog/[slug]/page.tsx
export default function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  return <article>Post: {params.slug}</article>;
}

Catch-All Routes

全捕获路由

typescript
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
export default function Shop({
  params
}: {
  params: { slug: string[] }
}) {
  return <div>Path: {params.slug.join('/')}</div>;
}
typescript
// app/shop/[...slug]/page.tsx - 匹配 /shop/a, /shop/a/b 等路径
export default function Shop({
  params
}: {
  params: { slug: string[] }
}) {
  return <div>Path: {params.slug.join('/')}</div>;
}

Optional Catch-All

可选全捕获路由

typescript
// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b
typescript
// app/shop/[[...slug]]/page.tsx - 匹配 /shop 以及 /shop/a, /shop/a/b

Route Groups

路由组

Group routes without affecting URL:
app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
└── (shop)/
    └── products/
        └── page.tsx      # /products
对路由进行分组而不影响URL:
app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx      # /about
│   └── contact/
│       └── page.tsx      # /contact
└── (shop)/
    └── products/
        └── page.tsx      # /products

Common Migration Pitfalls

常见迁移陷阱

Pitfall 1: Forgetting Root Layout HTML Tags

陷阱1:忘记根布局的HTML标签

Wrong:
typescript
export default function RootLayout({ children }) {
  return <div>{children}</div>; // Missing <html> and <body>
}
Correct:
typescript
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
错误:
typescript
export default function RootLayout({ children }) {
  return <div>{children}</div>; // 缺少<html>和<body>
}
正确:
typescript
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Pitfall 2: Using
next/head
in App Router

陷阱2:在App Router中使用
next/head

Wrong:
typescript
import Head from 'next/head';

export default function Page() {
  return (
    <>
      <Head><title>Title</title></Head>
      <main>Content</main>
    </>
  );
}
Correct:
typescript
export const metadata = { title: 'Title' };

export default function Page() {
  return <main>Content</main>;
}
错误:
typescript
import Head from 'next/head';

export default function Page() {
  return (
    <>
      <Head><title>Title</title></Head>
      <main>Content</main>
    </>
  );
}
正确:
typescript
export const metadata = { title: 'Title' };

export default function Page() {
  return <main>Content</main>;
}

Pitfall 3: Not Removing Pages Directory

陷阱3:未删除Pages目录

After migrating routes, remove the old
pages/
directory files to avoid confusion. The build will fail if you have conflicting routes.
迁移路由后,删除旧的
pages/
目录文件以避免混淆。如果存在冲突路由,构建会失败。

Pitfall 4: Missing
page.tsx
Files

陷阱4:缺少
page.tsx
文件

Routes are NOT accessible without a
page.tsx
file. Layouts alone don't create routes.
app/
├── blog/
│   ├── layout.tsx   # NOT a route
│   └── page.tsx     # This makes /blog accessible
没有
page.tsx
文件的路由无法访问。仅靠布局无法创建路由。
app/
├── blog/
│   ├── layout.tsx   # 不是路由
│   └── page.tsx     # 此文件使/blog可访问

Pitfall 5: Incorrect Link Usage

陷阱5:错误使用Link

Wrong:
typescript
<a href="/about">About</a>  // Works but causes full page reload
Correct:
typescript
import Link from 'next/link';
<Link href="/about">About</Link>  // Client-side navigation
错误:
typescript
<a href="/about">About</a>  # 可工作但会导致整页刷新
正确:
typescript
import Link from 'next/link';
<Link href="/about">About</Link>  # 客户端导航

Server Components vs Client Components

服务器组件 vs 客户端组件

Default: Server Components

默认:服务器组件

All components in
app/
are Server Components by default:
typescript
// app/page.tsx - Server Component (default)
export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  const json = await data.json();

  return <div>{json.title}</div>;
}
Benefits:
  • Can use async/await directly
  • Direct database/API access
  • Zero client-side JavaScript
  • Automatic code splitting
app/
目录中的所有组件默认都是服务器组件:
typescript
// app/page.tsx - 服务器组件(默认)
export default async function Page() {
  const data = await fetch('https://api.example.com/data');
  const json = await data.json();

  return <div>{json.title}</div>;
}
优势:
  • 可直接使用async/await
  • 可直接访问数据库/API
  • 无客户端JavaScript
  • 自动代码分割

Client Components

客户端组件

Use
'use client'
directive when you need:
  • Interactive elements (onClick, onChange, etc.)
  • React hooks (useState, useEffect, useContext, etc.)
  • Browser APIs (window, localStorage, etc.)
  • Event listeners
typescript
// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}
当需要以下功能时,使用
'use client'
指令:
  • 交互元素(onClick、onChange等)
  • React钩子(useState、useEffect、useContext等)
  • 浏览器API(window、localStorage等)
  • 事件监听器
typescript
// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Data Fetching Patterns

数据获取模式

Server Component Data Fetching

服务器组件数据获取

typescript
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Revalidate every hour
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
typescript
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 每小时重新验证一次
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Parallel Data Fetching

并行数据获取

typescript
export default async function Page() {
  // Fetch in parallel
  const [posts, users] = await Promise.all([
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/users').then(r => r.json()),
  ]);

  return (/* render */);
}
typescript
export default async function Page() {
  // 并行获取数据
  const [posts, users] = await Promise.all([
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/users').then(r => r.json()),
  ]);

  return (/* 渲染内容 */);
}

Static Site Generation with generateStaticParams

使用generateStaticParams进行静态站点生成

Overview

概述

generateStaticParams
is the App Router equivalent of
getStaticPaths
from the Pages Router. It generates static pages at build time for dynamic routes.
generateStaticParams
是App Router中对应Pages Router的
getStaticPaths
的API。它在构建时为动态路由生成静态页面。

Basic Usage

基础用法

typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  // Return array of params to pre-render
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Blog post {params.id}</article>;
}
Key Points:
  • Returns an array of objects with route parameter keys
  • Each object represents one page to pre-render at build time
  • Function must be exported and named
    generateStaticParams
  • Works ONLY in Server Components (no
    'use client'
    directive)
  • Replaces Pages Router's
    getStaticPaths
typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  // 返回要预渲染的参数数组
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Blog post {params.id}</article>;
}
关键点:
  • 返回包含路由参数字段的对象数组
  • 每个对象代表一个要在构建时预渲染的页面
  • 函数必须导出且命名为
    generateStaticParams
  • 仅在服务器组件中有效(无
    'use client'
    指令)
  • 替代Pages Router的
    getStaticPaths

Fetching Data for Static Params

为静态参数获取数据

typescript
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}
typescript
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({
  params
}: {
  params: { slug: string }
}) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Multiple Dynamic Segments

多个动态路由段

typescript
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
  const categories = await getCategories();

  const params = [];
  for (const category of categories) {
    const products = await getProducts(category.slug);
    for (const product of products) {
      params.push({
        category: category.slug,
        id: product.id,
      });
    }
  }

  return params;
}

export default function ProductPage({
  params
}: {
  params: { category: string; id: string }
}) {
  return <div>Category: {params.category}, Product: {params.id}</div>;
}
typescript
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
  const categories = await getCategories();

  const params = [];
  for (const category of categories) {
    const products = await getProducts(category.slug);
    for (const product of products) {
      params.push({
        category: category.slug,
        id: product.id,
      });
    }
  }

  return params;
}

export default function ProductPage({
  params
}: {
  params: { category: string; id: string }
}) {
  return <div>Category: {params.category}, Product: {params.id}</div>;
}

Dynamic Behavior Configuration

动态行为配置

typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

// Control behavior for non-pre-rendered paths
export const dynamicParams = true; // default - allows runtime generation
// export const dynamicParams = false; // returns 404 for non-pre-rendered paths

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Post {params.id}</article>;
}
Options:
  • dynamicParams = true
    (default): Non-pre-rendered paths generated on-demand
  • dynamicParams = false
    : Non-pre-rendered paths return 404
typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

// 控制非预渲染路径的行为
export const dynamicParams = true; // 默认 - 允许运行时生成
// export const dynamicParams = false; // 非预渲染路径返回404

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <article>Post {params.id}</article>;
}
选项:
  • dynamicParams = true
    (默认):非预渲染路径按需生成
  • dynamicParams = false
    :非预渲染路径返回404

Common Patterns

常见模式

Pattern 1: Simple ID-based routes
typescript
export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}
Pattern 2: Fetch from API
typescript
export async function generateStaticParams() {
  const items = await fetch('https://api.example.com/items').then(r => r.json());
  return items.map(item => ({ id: item.id }));
}
Pattern 3: Database query
typescript
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map(post => ({ slug: post.slug }));
}
模式1:基于ID的简单路由
typescript
export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
    { id: '3' },
  ];
}
模式2:从API获取
typescript
export async function generateStaticParams() {
  const items = await fetch('https://api.example.com/items').then(r => r.json());
  return items.map(item => ({ id: item.id }));
}
模式3:数据库查询
typescript
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map(post => ({ slug: post.slug }));
}

Migration from Pages Router

从Pages Router迁移

Before (Pages Router):
typescript
// pages/blog/[id].tsx
export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
    ],
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  return { props: { id: params.id } };
}
After (App Router):
typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
  ];
}

export const dynamicParams = false; // equivalent to fallback: false

export default function BlogPost({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}
之前(Pages Router):
typescript
// pages/blog/[id].tsx
export async function getStaticPaths() {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
    ],
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  return { props: { id: params.id } };
}
之后(App Router):
typescript
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  return [
    { id: '1' },
    { id: '2' },
  ];
}

export const dynamicParams = false; // 等价于fallback: false

export default function BlogPost({
  params
}: {
  params: { id: string }
}) {
  return <div>Post {params.id}</div>;
}

Common Mistakes to Avoid

常见错误避免

❌ Wrong: Using
'use client'
typescript
'use client'; // ERROR! generateStaticParams only works in Server Components

export async function generateStaticParams() {
  return [{ id: '1' }];
}
❌ Wrong: Using Pages Router pattern
typescript
export async function getStaticPaths() { // Wrong API!
  return { paths: [...], fallback: false };
}
❌ Wrong: Missing export keyword
typescript
async function generateStaticParams() { // Must be exported!
  return [{ id: '1' }];
}
✅ Correct: Clean Server Component
typescript
// app/blog/[id]/page.tsx
// No 'use client' directive

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

export default function Page({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}
CRITICAL IMPLEMENTATION NOTE:
When asked to "write" or "implement"
generateStaticParams
:
  • DO use the Edit or Write tool to modify the actual file
  • DO add the function to the existing page.tsx file
  • DO remove any TODO comments about generateStaticParams
  • DON'T just output code in markdown - actually implement it
  • DON'T show code without writing it to the file
❌ 错误:使用
'use client'
typescript
'use client'; // 错误!generateStaticParams仅在服务器组件中有效

export async function generateStaticParams() {
  return [{ id: '1' }];
}
❌ 错误:使用Pages Router模式
typescript
export async function getStaticPaths() { // 错误的API!
  return { paths: [...], fallback: false };
}
❌ 错误:缺少export关键字
typescript
async function generateStaticParams() { // 必须导出!
  return [{ id: '1' }];
}
✅ 正确:简洁的服务器组件
typescript
// app/blog/[id]/page.tsx
// 无'use client'指令

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }];
}

export default function Page({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}
重要实现说明:
当要求“编写”或“实现”
generateStaticParams
时:
  • 务必使用编辑或写入工具修改实际文件
  • 务必将函数添加到现有的page.tsx文件中
  • 务必删除所有关于generateStaticParams的TODO注释
  • 不要仅在markdown中输出代码 - 要实际实现它
  • 不要展示代码而不写入文件

Testing and Validation

测试与验证

When migrating or building with App Router, verify:
  1. Structure:
    • app/
      directory exists
    • Root
      layout.tsx
      exists with
      <html>
      and
      <body>
    • Each route has a
      page.tsx
      file
  2. Metadata:
    • No
      next/head
      imports in App Router
    • Metadata exported from pages or layouts
    • Metadata properly typed with
      Metadata
      type
  3. Navigation:
    • Using
      Link
      component from
      next/link
    • Not using plain
      <a>
      tags for internal navigation
  4. Cleanup:
    • No remaining page files in
      pages/
      directory
    • _app.tsx
      and
      _document.tsx
      removed
    • Old metadata patterns removed
迁移或使用App Router开发时,验证以下内容:
  1. 结构:
    • 存在
      app/
      目录
    • 存在包含
      <html>
      <body>
      的根
      layout.tsx
    • 每个路由都有
      page.tsx
      文件
  2. 元数据:
    • App Router中无
      next/head
      导入
    • 元数据从页面或布局导出
    • 元数据使用
      Metadata
      类型正确标注
  3. 导航:
    • 使用
      next/link
      中的
      Link
      组件
    • 内部导航不使用纯
      <a>
      标签
  4. 清理:
    • pages/
      目录中无剩余页面文件
    • 已删除
      _app.tsx
      _document.tsx
    • 已移除旧的元数据模式

Quick Reference

快速参考

File Structure Mapping

文件结构映射

Pages RouterApp RouterPurpose
pages/index.tsx
app/page.tsx
Home route
pages/about.tsx
app/about/page.tsx
About route
pages/[id].tsx
app/[id]/page.tsx
Dynamic route
pages/_app.tsx
app/layout.tsx
Global layout
pages/_document.tsx
app/layout.tsx
HTML structure
pages/api/hello.ts
app/api/hello/route.ts
API route
Pages RouterApp Router用途
pages/index.tsx
app/page.tsx
首页路由
pages/about.tsx
app/about/page.tsx
关于页路由
pages/[id].tsx
app/[id]/page.tsx
动态路由
pages/_app.tsx
app/layout.tsx
全局布局
pages/_document.tsx
app/layout.tsx
HTML结构
pages/api/hello.ts
app/api/hello/route.ts
API路由

Common Commands

常用命令

bash
undefined
bash
undefined

Create new Next.js app with App Router

使用App Router创建新的Next.js应用

npx create-next-app@latest my-app
npx create-next-app@latest my-app

Run development server

启动开发服务器

npm run dev
npm run dev

Build for production

构建生产版本

npm run build
npm run build

Start production server

启动生产服务器

npm start
undefined
npm start
undefined

Additional Resources

额外资源

For more advanced routing patterns (parallel routes, intercepting routes, route handlers), refer to the
nextjs-advanced-routing
skill.
For Server vs Client component best practices and anti-patterns, refer to the
nextjs-server-client-components
and
nextjs-anti-patterns
skills.
如需了解更高级的路由模式(并行路由、拦截路由、路由处理器),请参考
nextjs-advanced-routing
指南。
如需了解服务器与客户端组件的最佳实践和反模式,请参考
nextjs-server-client-components
nextjs-anti-patterns
指南。