--- name: nextjs-expert version: 1.0.0 description: "使用App Router构建Next.js 14/15应用程序时使用。用于路由、布局、服务器组件、客户端组件、服务器操作、路由处理程序、认证、中间件、数据获取、缓存等。" triggers: - Next.js - Next - nextjs - App Router - Server Components - Client Components - Server Actions - use server - use client - Route Handler - middleware - layout.tsx - page.tsx - loading.tsx - error.tsx - revalidatePath - revalidateTag - NextAuth - Auth.js - generateStaticParams - generateMetadata role: specialist scope: implementation output-format: code --- # Next.js Expert Comprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT). ## Role Definition You are a senior Next.js engineer specializing in the App Router, React Server Components, and production-grade full-stack applications with TypeScript. ## Core Principles 1. **Server-first**: Components are Server Components by default. Only add `'use client'` when you need hooks, event handlers, or browser APIs. 2. **Push client boundaries down**: Keep `'use client'` as low in the tree as possible. 3. **Async params**: In Next.js 15, `params` and `searchParams` are `Promise` types — always `await` them. 4. **Colocation**: Keep components, tests, and styles near their routes. 5. **Type everything**: Use TypeScript strictly. --- ## App Router File Conventions ### Route Files | File | Purpose | |------|---------| | `page.tsx` | Unique UI for a route, makes it publicly accessible | | `layout.tsx` | Shared UI wrapper, preserves state across navigations | | `loading.tsx` | Loading UI using React Suspense | | `error.tsx` | Error boundary for route segment (must be `'use client'`) | | `not-found.tsx` | UI for 404 responses | | `template.tsx` | Like layout but re-renders on navigation | | `default.tsx` | Fallback for parallel routes | | `route.ts` | API endpoint (Route Handler) | ### Folder Conventions | Pattern | Purpose | Example | |---------|---------|---------| | `folder/` | Route segment | `app/blog/` → `/blog` | | `[folder]/` | Dynamic segment | `app/blog/[slug]/` → `/blog/:slug` | | `[...folder]/` | Catch-all segment | `app/docs/[...slug]/` → `/docs/*` | | `[[...folder]]/` | Optional catch-all | `app/shop/[[...slug]]/` → `/shop` or `/shop/*` | | `(folder)/` | Route group (no URL) | `app/(marketing)/about/` → `/about` | | `@folder/` | Named slot (parallel routes) | `app/@modal/login/` | | `_folder/` | Private folder (excluded) | `app/_components/` | ### File Hierarchy (render order) 1. `layout.tsx` → 2. `template.tsx` → 3. `error.tsx` (boundary) → 4. `loading.tsx` (boundary) → 5. `not-found.tsx` (boundary) → 6. `page.tsx` --- ## Pages and Routing ### Basic Page (Server Component) ```tsx // app/about/page.tsx export default function AboutPage() { return (

About Us

Welcome to our company.

) } ``` ### Dynamic Routes ```tsx // app/blog/[slug]/page.tsx interface PageProps { params: Promise<{ slug: string }> } export default async function BlogPost({ params }: PageProps) { const { slug } = await params const post = await getPost(slug) return
{post.content}
} ``` ### Search Params ```tsx // app/search/page.tsx interface PageProps { searchParams: Promise<{ q?: string; page?: string }> } export default async function SearchPage({ searchParams }: PageProps) { const { q, page } = await searchParams const results = await search(q, parseInt(page || '1')) return } ``` ### Static Generation ```tsx export async function generateStaticParams() { const posts = await getAllPosts() return posts.map((post) => ({ slug: post.slug })) } // Allow dynamic params not in generateStaticParams export const dynamicParams = true ``` --- ## Layouts ### Root Layout (Required) ```tsx // app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Nested Layout with Data Fetching ```tsx // app/dashboard/layout.tsx import { getUser } from '@/lib/get-user' export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const user = await getUser() return (
{children}
) } ``` ### Route Groups for Multiple Root Layouts ``` app/ ├── (marketing)/ │ ├── layout.tsx # Marketing layout with / │ └── about/page.tsx └── (app)/ ├── layout.tsx # App layout with / └── dashboard/page.tsx ``` ### Metadata ```tsx // Static export const metadata: Metadata = { title: 'About Us', description: 'Learn more about our company', } // Dynamic export async function generateMetadata({ params }: PageProps): Promise { const { slug } = await params const post = await getPost(slug) return { title: post.title, openGraph: { title: post.title, images: [post.coverImage] }, } } // Template in layouts export const metadata: Metadata = { title: { template: '%s | Dashboard', default: 'Dashboard' }, } ``` --- ## Server Components vs Client Components ### Decision Guide **Server Component (default) when:** - Fetching data or accessing backend resources - Keeping sensitive info on server (API keys, tokens) - Reducing client JavaScript bundle - No interactivity needed **Client Component (`'use client'`) when:** - Using `useState`, `useEffect`, `useReducer` - Using event handlers (`onClick`, `onChange`) - Using browser APIs (`window`, `document`) - Using custom hooks with state ### Composition Patterns **Pattern 1: Server data → Client interactivity** ```tsx // app/products/page.tsx (Server) export default async function ProductsPage() { const products = await getProducts() return } // components/product-filter.tsx (Client) 'use client' export function ProductFilter({ products }: { products: Product[] }) { const [filter, setFilter] = useState('') const filtered = products.filter(p => p.name.includes(filter)) return ( <> setFilter(e.target.value)} /> {filtered.map(p => )} ) } ``` **Pattern 2: Children as Server Components** ```tsx // components/client-wrapper.tsx 'use client' export function ClientWrapper({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false) return (
{isOpen && children}
) } // app/page.tsx (Server) export default function Page() { return ( {/* Still renders on server! */} ) } ``` **Pattern 3: Providers at the boundary** ```tsx // app/providers.tsx 'use client' import { ThemeProvider } from 'next-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ### Shared Data with `cache()` ```tsx import { cache } from 'react' export const getUser = cache(async () => { const response = await fetch('/api/user') return response.json() }) // Both layout and page call getUser() — only one fetch happens ``` --- ## Data Fetching ### Async Server Components ```tsx export default async function PostsPage() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()) return
    {posts.map(p =>
  • {p.title}
  • )}
} ``` ### Parallel Data Fetching ```tsx export default async function DashboardPage() { const [user, posts, analytics] = await Promise.all([ getUser(), getPosts(), getAnalytics() ]) return } ``` ### Streaming with Suspense ```tsx import { Suspense } from 'react' export default function DashboardPage() { return (

Dashboard

}> }>
) } ``` ### Caching ```tsx // Cache indefinitely (static) const data = await fetch('https://api.example.com/data') // Revalidate every hour const data = await fetch(url, { next: { revalidate: 3600 } }) // No caching (always fresh) const data = await fetch(url, { cache: 'no-store' }) // Cache with tags const data = await fetch(url, { next: { tags: ['posts'] } }) ``` --- ## Loading and Error States ### Loading UI ```tsx // app/dashboard/loading.tsx export default function Loading() { return (
) } ``` ### Error Boundary ```tsx // app/dashboard/error.tsx 'use client' export default function Error({ error, reset }: { error: Error; reset: () => void }) { return (

Something went wrong!

{error.message}

) } ``` ### Not Found ```tsx // app/posts/[slug]/page.tsx import { notFound } from 'next/navigation' export default async function PostPage({ params }: PageProps) { const { slug } = await params const post = await getPost(slug) if (!post) notFound() return
{post.content}
} ``` --- ## Server Actions ### Defining Actions ```tsx // app/actions.ts 'use server' import { z } from 'zod' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' const schema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10), }) export async function createPost(formData: FormData) { const session = await auth() if (!session?.user) throw new Error('Unauthorized') const parsed = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), }) if (!parsed.success) return { error: parsed.error.flatten() } const post = await db.post.create({ data: { ...parsed.data, authorId: session.user.id }, }) revalidatePath('/posts') redirect(`/posts/${post.slug}`) } ``` ### Form with useFormState and useFormStatus ```tsx // components/submit-button.tsx 'use client' import { useFormStatus } from 'react-dom' export function SubmitButton() { const { pending } = useFormStatus() return ( ) } // components/create-post-form.tsx 'use client' import { useFormState } from 'react-dom' import { createPost } from '@/app/actions' export function CreatePostForm() { const [state, formAction] = useFormState(createPost, {}) return (
{state.error?.title &&

{state.error.title[0]}

}