一键导入
nextjs-idioms
Next.js App Router, RSC, Server Actions, ISR. For React see react-idioms. For TypeScript see typescript-idioms.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Next.js App Router, RSC, Server Actions, ISR. For React see react-idioms. For TypeScript see typescript-idioms.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Structured fault tolerance for coordinator agents. 5-level escalation ladder (Retry → Replace → Skip → Redistribute → Degrade), dead-man timers, degraded completion protocol, and cross-level escalation format. Load when orchestrating agents that may fail.
Structured code review protocol for inspecting code quality against the full rule set. Use when auditing code written by yourself or another agent, during the /audit workflow, or when the user asks for a code review.
Reusable convergence protocol for coordinator agents. Defines the BRIEFING → ITERATE → GATE → CONVERGE loop, context hygiene rules, self-succession protocol, turn budget, and handoff compression. Load when orchestrating multi-iteration workflows.
Pre-flight checklist and post-implementation self-review protocol. Use before generating any code (pre-flight) and after writing code but before verification (self-review) to catch issues early.
MECE task decomposition, file ownership enforcement, DAG-based execution, and safe merge protocol for intra-domain parallel dispatch. The safety invariants that prevent merge chaos when multiple agents write in parallel. Applies recursively at every nesting depth.
Shared protocols for all agents in the multi-agent pipeline: recursive nesting, pre-implementation restatement, parallel dispatch format, and agent definition cascade. Load this skill instead of inlining these protocols in every agent file.
| name | nextjs-idioms |
| description | Next.js App Router, RSC, Server Actions, ISR. For React see react-idioms. For TypeScript see typescript-idioms. |
| paths | ["**/next.config.*","**/app/**/page.tsx","**/app/**/layout.tsx","**/app/**/route.ts","**/middleware.ts"] |
Next.js (15+) rewards App Router, Server Components, and Server Actions. Idiomatic Next.js = server-first, streaming, edge-ready. Push logic to the server, keep the client thin.
Scope: Next.js-specific patterns only. For React:
@.agents/skills/react-idioms/SKILL.md. For TypeScript:@.agents/skills/typescript-idioms/SKILL.md. For project layout:references/project-structure.md.
'use client' only when: using hooks (useState, useEffect), attaching event handlers, calling browser APIs (window, localStorage), or wrapping third-party client libs.// app/tasks/page.tsx (Server)
export default async function TasksPage() {
const tasks = await getTasks();
return <TaskBoard tasks={tasks} />; // Client component for drag-and-drop
}
// features/task/components/task-board.tsx ('use client')
export function TaskBoard({ tasks }: { tasks: Task[] }) {
const [sorted, setSorted] = useState(tasks);
return <DndContext>...</DndContext>;
}
'use client' as deep as possible — never mark an entire page as client:
// ❌ 'use client' at page level loses all server benefits
// ✅ Only wrap the interactive leaf:
export default async function TasksPage() {
const tasks = await getTasks();
return (
<>
<TaskStats count={tasks.length} /> {/* Server */}
<TaskFilterBar /> {/* Client — has state */}
</>
);
}
'use client' only per the decision tree above.app/tasks/
├── page.tsx # Server Component
├── loading.tsx # Suspense fallback
├── error.tsx # Error boundary ('use client')
└── layout.tsx # Shared layout
app/
├── (auth)/login/page.tsx # /login
├── (auth)/register/page.tsx # /register
└── (dashboard)/
├── layout.tsx # Shared dashboard layout
├── tasks/page.tsx # /tasks
└── settings/page.tsx # /settings
@slot parallel routes — render multiple pages simultaneously in the same layout:
app/(dashboard)/
├── layout.tsx # Receives { children, modal }
├── @modal/default.tsx # Required: null fallback
├── @modal/(.)tasks/[id]/page.tsx # Intercepting route → modal
├── tasks/page.tsx # Main content
└── tasks/[id]/page.tsx # Full page (direct nav)
export default function DashboardLayout({
children, modal,
}: {
children: React.ReactNode; modal: React.ReactNode;
}) {
return <>{children}{modal}</>;
}
default.tsx is required for every @slot — returns null when no active match.(.) same level, (..) one level up, (...) from root.export default async function TasksPage() {
const tasks = await db.tasks.findMany();
return <TaskList tasks={tasks} />;
}
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createTask(formData: FormData) {
const title = formData.get('title');
if (!title || typeof title !== 'string') return { error: 'Title is required' };
await db.tasks.create({ data: { title } });
revalidatePath('/tasks');
redirect('/tasks');
}
await:
const [user, tasks, stats] = await Promise.all([getUser(), getTasks(), getStats()]);
fetch cache options — Next.js extends fetch:
await fetch(url, { cache: 'force-cache' }); // Cached indefinitely
await fetch(url, { cache: 'no-store' }); // Fresh every request
await fetch(url, { next: { revalidate: 3600 } }); // Time-based ISR
await fetch(url, { next: { tags: ['tasks'] } }); // Tag-based invalidation
'use cache' directive for non-fetch data (DB queries, computations — Next.js 15+):
'use cache';
import { cacheLife, cacheTag } from 'next/cache';
export async function getCachedTasks(userId: string) {
cacheLife('minutes'); // Built-in profile: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'max'
cacheTag('tasks', `user-${userId}`);
return db.tasks.findMany({ where: { userId } });
}
Legacy:
unstable_cache(deprecated in Next.js 15+) works the same way but is being replaced by'use cache'.
export const revalidate = 60; // ISR every 60s
export const dynamic = 'force-dynamic'; // Always fresh
'use server';
export async function updateTask(id: string, data: TaskUpdate) {
await db.tasks.update({ where: { id }, data });
revalidateTag('tasks'); // Invalidate tagged fetches
revalidatePath('/tasks'); // Rebuild the page
}
force-cache. User-specific → no-store. Semi-dynamic → revalidate: N. After mutation → revalidateTag/revalidatePath.// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const createTaskSchema = z.object({
title: z.string().min(1).max(200),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
export async function GET(request: NextRequest) {
const tasks = await db.tasks.findMany();
return NextResponse.json(tasks);
}
export async function POST(request: NextRequest) {
const parsed = createTaskSchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
}
const task = await db.tasks.create({ data: parsed.data });
return NextResponse.json(task, { status: 201 });
}
// app/api/tasks/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const task = await db.tasks.findUnique({ where: { id } });
if (!task) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json(task);
}
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of db.tasks.stream()) {
controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n'));
}
controller.close();
},
});
return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } });
}
middleware.ts at project root (or src/middleware.ts):
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
const response = NextResponse.next();
response.headers.set('x-request-id', crypto.randomUUID());
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
config.matcher — never run middleware on every request.fs, path). Web APIs only.NEXT_PUBLIC_ prefix exposes vars to client — use only for non-secrets:
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ Client + server
const dbUrl = process.env.DATABASE_URL; // ✅ Server-only
// src/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
SESSION_SECRET: z.string().min(32),
NEXT_PUBLIC_API_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = envSchema.parse(process.env);
process.env in business logic — import from validated env module..env.local for local overrides (gitignored). .env for defaults (committed, no secrets).error.tsx boundary ('use client' required) with reset for retry:
'use client';
export default function ErrorBoundary({ error, reset }: {
error: Error & { digest?: string }; reset: () => void;
}) {
return <div><h2>Something went wrong</h2><button onClick={reset}>Retry</button></div>;
}
not-found.tsx for 404 — call notFound() when data is missing:
import { notFound } from 'next/navigation';
export default async function TaskPage({ params }: { params: Promise<{ id: string }> }) {
const task = await getTask((await params).id);
if (!task) notFound();
return <TaskDetail task={task} />;
}
'use server';
type ActionResult = { success: true } | { success: false; error: string };
export async function createTask(formData: FormData): Promise<ActionResult> {
try {
await db.tasks.create({ data: { title: formData.get('title') as string } });
revalidatePath('/tasks');
return { success: true };
} catch { return { success: false, error: 'Failed to create task' }; }
}
export const dynamic = 'force-dynamic' only when data changes per request.next/image with width, height, and priority for above-fold.next/link.<Suspense fallback={<TasksSkeleton />}>
<TaskList /> {/* Server Component — streams when ready */}
</Suspense>
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Tasks | MyApp',
description: 'Manage your tasks efficiently',
openGraph: { title: 'Tasks', description: 'Manage your tasks efficiently', type: 'website' },
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
const task = await getTask(id);
return { title: task.title, description: task.description };
}
useEffect for data fetching in Server Components — fetch directly'use client' on everything — Server Components are the default for a reasongetServerSideProps / getStaticProps — App Router uses async componentsawait in Server Components — use Promise.all() for parallel fetching'use client' components small, push logic to serverfetch URLs — use environment variables and centralized API clientprocess.env everywhere — validate once in env.ts, import the typed objectconfig.matcher to limit to relevant routesFor universal testing principles, see
.agents/rules/testing-strategy.md. Below: framework-specific patterns only.
next/jest for jest configuration:
const nextJest = require('next/jest')({ dir: './' });
module.exports = nextJest({ /* custom config */ });
import { createTask } from '@/app/actions';
it('returns error for empty title', async () => {
const formData = new FormData();
formData.set('title', '');
const result = await createTask(formData);
expect(result).toEqual({ error: 'Title is required' });
});
import { GET } from '@/app/api/tasks/route';
it('returns tasks as JSON', async () => {
const request = new NextRequest('http://localhost/api/tasks');
const response = await GET(request);
expect(response.status).toBe(200);
});
import { middleware } from '@/middleware';
it('redirects unauthenticated users', () => {
const request = new NextRequest('http://localhost/dashboard');
const response = middleware(request);
expect(response.status).toBe(307);
expect(response.headers.get('location')).toContain('/login');
});
| Tool | Purpose | Command |
|---|---|---|
| Prettier | Formatting | npx prettier --write . |
ESLint + eslint-config-next | Linting | npx next lint |
| TypeScript | Type checking | npx tsc --noEmit |