nextjs-app-router-fundamentals by wsimmonds/claude-nextjs-skills
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-app-router-fundamentals为 Next.js App Router(Next.js 13+)提供全面的指导,涵盖从 Pages Router 迁移、基于文件的路由约定、布局、元数据处理以及现代 Next.js 模式。
any 类型关键规则: 此代码库已启用 @typescript-eslint/no-explicit-any。使用 any 将导致构建失败。
❌ 错误:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ 正确:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
// 页面属性
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) { ... }
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在以下情况下使用此技能:
pages/ 目录)迁移到 App Router(app/ 目录)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
App Router 中的特殊文件:
layout.tsx - 用于片段及其子级的共享 UI(保留状态,不重新渲染)page.tsx - 路由的唯一 UI,使路由可公开访问loading.tsx - 使用 React Suspense 的加载 UIerror.tsx - 使用错误边界的错误 UInot-found.tsx - 404 UItemplate.tsx - 类似于布局,但在导航时重新渲染route.ts - API 端点(路由处理器)同位置存放:
app/ 中page.tsx 和 route.ts 文件会创建公共路由检查现有的 Pages Router 设置:
pages/ 目录结构_app.tsx - 处理全局状态、布局、提供者_document.tsx - 自定义 HTML 结构next/head、<Head> 组件)创建 app/layout.tsx - 所有 App Router 应用程序必需:
// 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> 标签简单页面迁移:
// 之前:pages/index.tsx
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>Home Page</title>
</Head>
<main>
<h1>Welcome</h1>
</main>
</>
);
}
// 之后:app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome</h1>
</main>
);
}
// 元数据已移动到 layout.tsx 或在此处导出
export const metadata = {
title: 'Home Page',
};
嵌套路由迁移:
// 之前:pages/blog/[slug].tsx
export default function BlogPost() { ... }
// 之后:app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
将锚标签替换为 Next.js Link:
// 之前(在 App Router 中不正确)
<a href="/about">About</a>
// 之后(正确)
import Link from 'next/link';
<Link href="/about">About</Link>
迁移后:
pages/ 目录中删除所有页面文件pages/api/_app.tsx 和 _document.tsx(功能已移动到布局)pages/ 目录// 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'],
},
};
// 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,
};
}
// 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>
);
}
布局行为:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return <article>Post: {params.slug}</article>;
}
// 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>;
}
// app/shop/[[...slug]]/page.tsx - 匹配 /shop 以及 /shop/a、/shop/a/b
分组路由而不影响 URL:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
└── products/
└── page.tsx # /products
错误:
export default function RootLayout({ children }) {
return <div>{children}</div>; // 缺少 <html> 和 <body>
}
正确:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
next/head错误:
import Head from 'next/head';
export default function Page() {
return (
<>
<Head><title>Title</title></Head>
<main>Content</main>
</>
);
}
正确:
export const metadata = { title: 'Title' };
export default function Page() {
return <main>Content</main>;
}
迁移路由后,删除旧的 pages/ 目录文件以避免混淆。如果存在冲突的路由,构建将失败。
page.tsx 文件没有 page.tsx 文件,路由将无法访问。仅靠布局不会创建路由。
app/
├── blog/
│ ├── layout.tsx # 不是路由
│ └── page.tsx # 这使得 /blog 可访问
错误:
<a href="/about">About</a> // 有效但会导致整个页面重新加载
正确:
import Link from 'next/link';
<Link href="/about">About</Link> // 客户端导航
app/ 中的所有组件默认都是服务器组件:
// 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>;
}
优势:
当需要以下功能时,使用 'use client' 指令:
交互式元素(onClick、onChange 等)
React 钩子(useState、useEffect、useContext 等)
浏览器 API(window、localStorage 等)
事件监听器
// 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> ); }
// 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>
);
}
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 (/* 渲染 */);
}
generateStaticParams 是 App Router 中相当于 Pages Router 的 getStaticPaths 的功能。它在构建时为动态路由生成静态页面。
// 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' 指令)getStaticPaths// 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>
);
}
// 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>;
}
// 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模式 1:基于简单 ID 的路由
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
模式 2:从 API 获取
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:数据库查询
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map(post => ({ slug: post.slug }));
}
之前(Pages Router):
// 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):
// 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>;
}
❌ 错误:使用 'use client'
'use client'; // 错误!generateStaticParams 仅适用于服务器组件
export async function generateStaticParams() {
return [{ id: '1' }];
}
❌ 错误:使用 Pages Router 模式
export async function getStaticPaths() { // 错误的 API!
return { paths: [...], fallback: false };
}
❌ 错误:缺少导出关键字
async function generateStaticParams() { // 必须导出!
return [{ id: '1' }];
}
✅ 正确:干净的服务器组件
// 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 时:
迁移或使用 App Router 构建时,请验证:
结构:
app/ 目录layout.tsx 存在且包含 <html> 和 <body>page.tsx 文件元数据:
next/head 导入Metadata 类型正确键入导航:
next/link 的 Link 组件<a> 标签清理:
pages/ 目录中没有剩余的页面文件_app.tsx 和 _document.tsx| Pages Router | App 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 路由 |
# 使用 App Router 创建新的 Next.js 应用
npx create-next-app@latest my-app
# 运行开发服务器
npm run dev
# 为生产环境构建
npm run build
# 启动生产服务器
npm start
有关更高级的路由模式(并行路由、拦截路由、路由处理器),请参阅 nextjs-advanced-routing 技能。
有关服务器与客户端组件的最佳实践和反模式,请参阅 nextjs-server-client-components 和 nextjs-anti-patterns 技能。
每周安装次数
1.8K
仓库
GitHub 星标数
80
首次出现时间
Jan 23, 2026
安全审计
安装于
opencode1.5K
codex1.5K
gemini-cli1.5K
github-copilot1.5K
kimi-cli1.5K
amp1.5K
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.
any TypeCRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.
❌ WRONG:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ CORRECT:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
// 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) { ... }
Use this skill when:
pages/ directory) to App Router (app/ directory)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
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 accessibleloading.tsx - Loading UI with React Suspenseerror.tsx - Error UI with Error Boundariesnot-found.tsx - 404 UItemplate.tsx - Similar to layout but re-renders on navigationroute.ts - API endpoints (Route Handlers)Colocation:
app/page.tsx and route.ts files create public routesExamine existing Pages Router setup:
pages/ directory structure_app.tsx - handles global state, layouts, providers_document.tsx - customizes HTML structurenext/head, <Head> component)Create app/layout.tsx - REQUIRED for all App Router applications:
// 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:
_document.tsx HTML structure to layout.tsx_app.tsx global providers/wrappers to layout.tsx<Head> metadata to metadata export<html> and <body> tagsSimple Page Migration:
// 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>
</>
);
}
// 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:
// Before: pages/blog/[slug].tsx
export default function BlogPost() { ... }
// After: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
Replace anchor tags with Next.js Link:
// Before (incorrect in App Router)
<a href="/about">About</a>
// After (correct)
import Link from 'next/link';
<Link href="/about">About</Link>
After migration:
pages/ directorypages/api/ if you're not migrating API routes yet_app.tsx and _document.tsx (functionality moved to layout)pages/ directory// 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'],
},
};
// 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,
};
}
// 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:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return <article>Post: {params.slug}</article>;
}
// 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>;
}
// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b
Group routes without affecting URL:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
└── products/
└── page.tsx # /products
Wrong:
export default function RootLayout({ children }) {
return <div>{children}</div>; // Missing <html> and <body>
}
Correct:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
next/head in App RouterWrong:
import Head from 'next/head';
export default function Page() {
return (
<>
<Head><title>Title</title></Head>
<main>Content</main>
</>
);
}
Correct:
export const metadata = { title: 'Title' };
export default function Page() {
return <main>Content</main>;
}
After migrating routes, remove the old pages/ directory files to avoid confusion. The build will fail if you have conflicting routes.
page.tsx FilesRoutes 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
Wrong:
<a href="/about">About</a> // Works but causes full page reload
Correct:
import Link from 'next/link';
<Link href="/about">About</Link> // Client-side navigation
All components in app/ are Server Components by default:
// 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:
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
// 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> ); }
// 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>
);
}
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 */);
}
generateStaticParams is the App Router equivalent of getStaticPaths from the Pages Router. It generates static pages at build time for dynamic routes.
// 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:
generateStaticParams'use client' directive)getStaticPaths// 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>
);
}
// 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>;
}
// 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-demanddynamicParams = false: Non-pre-rendered paths return 404Pattern 1: Simple ID-based routes
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
Pattern 2: Fetch from API
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
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map(post => ({ slug: post.slug }));
}
Before (Pages Router):
// 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):
// 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>;
}
❌ Wrong: Using'use client'
'use client'; // ERROR! generateStaticParams only works in Server Components
export async function generateStaticParams() {
return [{ id: '1' }];
}
❌ Wrong: Using Pages Router pattern
export async function getStaticPaths() { // Wrong API!
return { paths: [...], fallback: false };
}
❌ Wrong: Missing export keyword
async function generateStaticParams() { // Must be exported!
return [{ id: '1' }];
}
✅ Correct: Clean Server Component
// 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:
When migrating or building with App Router, verify:
Structure:
app/ directory existslayout.tsx exists with <html> and <body>page.tsx fileMetadata:
next/head imports in App RouterMetadata typeNavigation:
Link component from | Pages Router | App Router | Purpose |
|---|---|---|
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 |
# Create new Next.js app with App Router
npx create-next-app@latest my-app
# Run development server
npm run dev
# Build for production
npm run build
# Start production server
npm start
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.
Weekly Installs
1.8K
Repository
GitHub Stars
80
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode1.5K
codex1.5K
gemini-cli1.5K
github-copilot1.5K
kimi-cli1.5K
amp1.5K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
next/link<a> tags for internal navigationCleanup:
pages/ directory_app.tsx and _document.tsx removedapp/layout.tsx |
| Global layout |
pages/_document.tsx | app/layout.tsx | HTML structure |
pages/api/hello.ts | app/api/hello/route.ts | API route |