<!-- skill: Next.js App | version: 1.0.0 | description: Ship Next.js App Router apps with server components and caching | category: Web -->
## Instructions
- App Router only: `app/` directory, `page.tsx`/`layout.tsx`/`loading.tsx`/`error.tsx`/`not-found.tsx` conventions; `layout` persists nav, `loading` streams Suspense fallback.
- Server vs Client: Server Components by default (async, direct DB/API reads, zero JS); `'use client'` only for hooks/events/browser APIs — push it to leaf components.
- Data fetching: `async` Server Components + `fetch(url, { next: { revalidate: 60 } })`; `cache()` for dedup, `revalidatePath`/`revalidateTag` after mutations; no `useEffect` fetching.
- Route handlers: `app/api/*/route.ts` exporting `GET/POST` with `NextRequest/NextResponse`, Zod-validate bodies, return typed JSON + status; use for webhooks/forms, not for internal-only reads.
- Server Actions: `'use server'` functions called from forms/components for mutations; validate + auth inside, `redirect()`/`revalidatePath()` after; progressive enhancement via `<form action>`.
- Middleware: `middleware.ts` at root for auth redirects, locale, A/B headers via `NextResponse.rewrite/redirect`; keep it edge-light (no heavy imports/DB).
- Metadata/SEO: `export const metadata` per page, `generateMetadata` for dynamic, `sitemap.ts` + `robots.ts`, `next/image` with `sizes` + `alt`, `next/font` self-hosted.
- Styling: Tailwind or CSS modules, `clsx` for variants; dark mode via `class` strategy; check `next build` CSS size, avoid global `*` resets.
- Errors: `error.tsx` boundaries per segment, `not-found()` for 404s, global `global-error.tsx`; log server errors with request id, show generic UI to users.
- Env: `NEXT_PUBLIC_` only for browser-safe vars; server secrets in `.env.local` (never committed); validate with `zod` at boot (`env.ts` module).
- Deploy Vercel: `vercel` git-connected, preview per PR, `ISR + revalidateTag` for CMS content; self-host: `next build && next start` node 20, `standalone` output + reverse proxy.
- Perf: `next build` must pass, Lighthouse >90; `dynamic(() => import(...))` for heavy client islands, `loading.tsx` skeletons, `fetch` cache over client state.
## Examples
- Blog + CMS: async post pages with `revalidate: 300`, `generateStaticParams` for slugs, `revalidateTag('posts')` webhook from CMS route handler.
- Auth area: middleware gates `/dashboard`, Server Action login with httpOnly cookie, `getUser()` cached per request, client island for interactive chart.
- Form + upload: `<form action={createPost}>` Server Action, Zod validation, error return rendered inline, `redirect('/posts/slug')` on success.
## Workflows
1. Scaffold: `npx create-next-app@latest` (TS, App Router, Tailwind), set `env.ts` + metadata.
2. Build routes: layouts/pages/loading/error per segment, server-first components.
3. Add data layer: cached fetches, `generateStaticParams`, revalidation tags.
4. Add mutations: Server Actions + route handlers (webhooks), Zod + auth checks.
5. Harden: middleware, not-found/error UI, `next build` + `next lint` clean.
6. Deploy Vercel (git previews) or `output: 'standalone'` node self-host + proxy TLS.
## Templates
- Server page with caching + static params:
```tsx
// app/posts/[slug]/page.tsx
import { cache } from 'react';
import { notFound } from 'next/navigation';
export const revalidate = 300;
const getPost = cache(async (slug: string) => {
  const r = await fetch(`https://cms.example.com/posts/${slug}`,
    { next: { tags: ['posts'] } });
  if (r.status === 404) return null;
  if (!r.ok) throw new Error('cms failed');
  return r.json();
});
export async function generateStaticParams() {
  const r = await fetch('https://cms.example.com/posts?limit=50');
  const posts = await r.json();
  return posts.map((p: any) => ({ slug: p.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return { title: post ? post.title : 'Not found' };
}
export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  if (!post) notFound();
  return <article><h1>{post.title}</h1><div>{post.body}</div></article>;
}
```
- Server Action mutation:
```tsx
// app/posts/new/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(3), body: z.string().min(10) });
export async function createPost(formData: FormData) {
  const parsed = Schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors };
  const slug = await savePost(parsed.data); // your DB call + auth check
  revalidatePath('/posts');
  redirect(`/posts/${slug}`);
}
```
- Middleware + route handler:
```ts
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
  if (req.nextUrl.pathname.startsWith('/dashboard') && !req.cookies.has('session')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}
export const config = { matcher: ['/dashboard/:path*'] };
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(req: NextRequest) {
  if (req.headers.get('x-webhook-secret') !== process.env.CMS_SECRET)
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  revalidateTag('posts');
  return NextResponse.json({ revalidated: true });
}
```
## Tools
- SARAH shell_run for `npm run dev/build/lint`, `npx tsc --noEmit`, Lighthouse, `vercel deploy`.
- SARAH file_write for pages/layouts/actions/middleware; web_fetch for Next.js docs + CMS webhooks.
## Advanced
- Partial prerender: `experimental_ppr` + Suspense shells, streaming `loading.tsx`, `unstable_cache` for expensive renders; edge runtime only for tiny handlers.
- Bundle diet: `next/bundle-analyzer`, `dynamic(() => import('./HeavyChart'), { ssr: false })`, `next/image` AVIF, `optimizePackageImports` for icon libs.
- Self-host ops: `output: 'standalone'`, `Dockerfile` node:20-alpine, `HEALTHCHECK /api/health`, PM2/systemd + Nginx proxy + `next start -p 3000`.
## Documentation
- https://nextjs.org/docs
- https://nextjs.org/docs/app
- https://react.dev/reference/rsc/server-components
- https://vercel.com/docs
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
