nextjs-app-router-fundamentals
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNext.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
anyTypeScript:切勿使用any
类型
anyCRITICAL RULE: This codebase has enabled. Using will cause build failures.
@typescript-eslint/no-explicit-anyany❌ WRONG:
typescript
function handleSubmit(e: any) { ... }
const data: any[] = [];✅ CORRECT:
typescript
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];重要规则: 此代码库已启用规则。使用类型会导致构建失败。
@typescript-eslint/no-explicit-anyany❌ 错误示例:
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 (directory) to App Router (
pages/directory)app/ - 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(目录)迁移到App Router(
pages/目录)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/helloApp 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/helloPages Router(旧版 - Next.js 12及更早版本):
pages/
├── index.tsx # 路由: /
├── about.tsx # 路由: /about
├── _app.tsx # 自定义App组件
├── _document.tsx # 自定义Document组件
└── api/ # API路由
└── hello.ts # API端点: /api/helloApp 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/helloFile Conventions
文件约定
Special Files in App Router:
- - Shared UI for a segment and its children (preserves state, doesn't re-render)
layout.tsx - - Unique UI for a route, makes route publicly accessible
page.tsx - - Loading UI with React Suspense
loading.tsx - - Error UI with Error Boundaries
error.tsx - - 404 UI
not-found.tsx - - Similar to layout but re-renders on navigation
template.tsx - - API endpoints (Route Handlers)
route.ts
Colocation:
- Components, tests, and other files can be colocated in
app/ - Only and
page.tsxfiles create public routesroute.ts - Other files (components, utils, tests) are NOT routable
App Router中的特殊文件:
- - 为某个路由段及其子路由提供共享UI(保留状态,不会重新渲染)
layout.tsx - - 路由的专属UI,使路由可公开访问
page.tsx - - 基于React Suspense的加载UI
loading.tsx - - 基于Error Boundaries的错误UI
error.tsx - - 404页面UI
not-found.tsx - - 与布局类似,但在导航时会重新渲染
template.tsx - - API端点(路由处理器)
route.ts
就近放置:
- 组件、测试和其他文件可放置在目录中
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 directory structure
pages/ - Identify - handles global state, layouts, providers
_app.tsx - Identify - customizes HTML structure
_document.tsx - Note metadata usage (,
next/headcomponent)<Head> - List all routes and dynamic segments
检查现有Pages Router设置:
- 查看目录结构
pages/ - 找到- 处理全局状态、布局、提供者
_app.tsx - 找到- 自定义HTML结构
_document.tsx - 记录元数据的使用方式(、
next/head组件)<Head> - 列出所有路由和动态路由段
Step 2: Create Root Layout
步骤2:创建根布局
Create - REQUIRED for all App Router applications:
app/layout.tsxtypescript
// 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 HTML structure to
_document.tsxlayout.tsx - Move global providers/wrappers to
_app.tsxlayout.tsx - Convert metadata to
<Head>exportmetadata - The root layout MUST include and
<html>tags<body>
创建 - 所有App Router应用都必填:
app/layout.tsxtypescript
// 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>
);
}迁移注意事项:
- 将中的HTML结构迁移到
_document.tsxlayout.tsx - 将中的全局提供者/包装器迁移到
_app.tsxlayout.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 directory
pages/ - Keep if you're not migrating API routes yet
pages/api/ - Remove and
_app.tsx(functionality moved to layout)_document.tsx - Optionally delete empty directory
pages/
迁移完成后:
- 删除目录中的所有页面文件
pages/ - 如果尚未迁移API路由,可保留
pages/api/ - 删除和
_app.tsx(功能已迁移到layout)_document.tsx - 可选择删除空的目录
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/btypescript
// app/shop/[[...slug]]/page.tsx - 匹配 /shop 以及 /shop/a, /shop/a/bRoute 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 # /productsCommon 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
next/head陷阱2:在App Router中使用next/head
next/headWrong:
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 directory files to avoid confusion. The build will fail if you have conflicting routes.
pages/迁移路由后,删除旧的目录文件以避免混淆。如果存在冲突路由,构建会失败。
pages/Pitfall 4: Missing page.tsx
Files
page.tsx陷阱4:缺少page.tsx
文件
page.tsxRoutes are NOT accessible without a file. Layouts alone don't create routes.
page.tsxapp/
├── blog/
│ ├── layout.tsx # NOT a route
│ └── page.tsx # This makes /blog accessible没有文件的路由无法访问。仅靠布局无法创建路由。
page.tsxapp/
├── 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 reloadCorrect:
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 are Server Components by default:
app/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 directive when you need:
'use client'- 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
概述
generateStaticParamsgetStaticPathsgenerateStaticParamsgetStaticPathsBasic 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 directive)
'use client' - 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:
- (default): Non-pre-rendered paths generated on-demand
dynamicParams = true - : Non-pre-rendered paths return 404
dynamicParams = false
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 - :非预渲染路径返回404
dynamicParams = false
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:
-
Structure:
- directory exists
app/ - Root exists with
layout.tsxand<html><body> - Each route has a file
page.tsx
-
Metadata:
- No imports in App Router
next/head - Metadata exported from pages or layouts
- Metadata properly typed with type
Metadata
- No
-
Navigation:
- Using component from
Linknext/link - Not using plain tags for internal navigation
<a>
- Using
-
Cleanup:
- No remaining page files in directory
pages/ - and
_app.tsxremoved_document.tsx - Old metadata patterns removed
- No remaining page files in
迁移或使用App Router开发时,验证以下内容:
-
结构:
- 存在目录
app/ - 存在包含和
<html>的根<body>layout.tsx - 每个路由都有文件
page.tsx
- 存在
-
元数据:
- App Router中无导入
next/head - 元数据从页面或布局导出
- 元数据使用类型正确标注
Metadata
- App Router中无
-
导航:
- 使用中的
next/link组件Link - 内部导航不使用纯标签
<a>
- 使用
-
清理:
- 目录中无剩余页面文件
pages/ - 已删除和
_app.tsx_document.tsx - 已移除旧的元数据模式
Quick Reference
快速参考
File Structure Mapping
文件结构映射
| Pages Router | App Router | Purpose |
|---|---|---|
| | Home route |
| | About route |
| | Dynamic route |
| | Global layout |
| | HTML structure |
| | API route |
| Pages Router | App Router | 用途 |
|---|---|---|
| | 首页路由 |
| | 关于页路由 |
| | 动态路由 |
| | 全局布局 |
| | HTML结构 |
| | API路由 |
Common Commands
常用命令
bash
undefinedbash
undefinedCreate 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
undefinednpm start
undefinedAdditional Resources
额外资源
For more advanced routing patterns (parallel routes, intercepting routes, route handlers), refer to the skill.
nextjs-advanced-routingFor Server vs Client component best practices and anti-patterns, refer to the and skills.
nextjs-server-client-componentsnextjs-anti-patterns如需了解更高级的路由模式(并行路由、拦截路由、路由处理器),请参考指南。
nextjs-advanced-routing如需了解服务器与客户端组件的最佳实践和反模式,请参考和指南。
nextjs-server-client-componentsnextjs-anti-patterns