Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-06-24 11:57:13 +08:00
parent 0f46963526
commit 0d5600791f
41 changed files with 830 additions and 4102 deletions
+96 -96
View File
@@ -160,6 +160,90 @@
},
"category": "文档处理"
},
{
"name": "eapil-llmapi-structured",
"source": {
"source": "local",
"path": "./plugins/eapil-llmapi-structured"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "generate-agents-md",
"source": {
"source": "local",
"path": "./plugins/generate-agents-md"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "知识库与检索"
},
{
"name": "eapil-voice",
"source": {
"source": "local",
"path": "./plugins/eapil-voice"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "多媒体与生成"
},
{
"name": "superpowers",
"source": {
"source": "local",
"path": "./plugins/superpowers"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "oh-my-codex",
"source": {
"source": "local",
"path": "./plugins/oh-my-codex"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "ui-ux-pro-max",
"source": {
"source": "local",
"path": "./plugins/ui-ux-pro-max"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "设计"
},
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "frontend-slides",
"source": {
@@ -196,6 +280,18 @@
},
"category": "文档处理"
},
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{
"name": "next-skills",
"source": {
@@ -208,54 +304,6 @@
},
"category": "开发工具"
},
{
"name": "eapil-llmapi-structured",
"source": {
"source": "local",
"path": "./plugins/eapil-llmapi-structured"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "generate-agents-md",
"source": {
"source": "local",
"path": "./plugins/generate-agents-md"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "知识库与检索"
},
{
"name": "superpowers",
"source": {
"source": "local",
"path": "./plugins/superpowers"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "oh-my-codex",
"source": {
"source": "local",
"path": "./plugins/oh-my-codex"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "mcp-playwright",
"source": {
@@ -267,54 +315,6 @@
"authentication": "ON_INSTALL"
},
"category": "MCP"
},
{
"name": "ui-ux-pro-max",
"source": {
"source": "local",
"path": "./plugins/ui-ux-pro-max"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "设计"
},
{
"name": "shadcn",
"source": {
"source": "local",
"path": "./plugins/shadcn"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "开发工具"
},
{
"name": "ppt-master",
"source": {
"source": "local",
"path": "./plugins/ppt-master"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "文档处理"
},
{
"name": "eapil-voice",
"source": {
"source": "local",
"path": "./plugins/eapil-voice"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "多媒体与生成"
}
]
}
@@ -2,6 +2,16 @@
A coding-agent skill for creating stunning HTML presentations — from scratch or by converting PowerPoint files. It is packaged as a Claude Code plugin, and the core `SKILL.md` can also be read by other coding agents with filesystem and shell access.
## 📺 Watch the Walkthrough & Tutorial
New here? This beginner-friendly video walks you through the whole thing, start to finish.
<a href="https://www.youtube.com/watch?v=372Iksaz8b0" title="Frontend Slides — walkthrough & tutorial (beginner-friendly)">
<img src="https://img.youtube.com/vi/372Iksaz8b0/maxresdefault.jpg" alt="Watch the Frontend Slides walkthrough and tutorial on YouTube" width="100%" />
</a>
> ▶️ **[Watch on YouTube →](https://www.youtube.com/watch?v=372Iksaz8b0)** (beginner-friendly walkthrough and tutorial)
## What This Does
**Frontend Slides** helps non-designers create beautiful web presentations without knowing CSS or JavaScript. It uses a "show, don't tell" approach: instead of asking you to describe your aesthetic preferences in words, it generates visual previews and lets you pick what you like.
@@ -2,8 +2,8 @@
"sourceId": "frontend-slides",
"repo": "https://github.com/zarazhangrui/frontend-slides.git",
"ref": "main",
"commit": "c28690100a289d979e5e032b44771a7c28a86e40",
"commit": "9906a34d640d2111f724544cbc50f7f130569ae1",
"adapter": "claude-skill",
"sourcePath": "plugins/frontend-slides/skills/frontend-slides",
"syncedAt": "2026-06-13T16:00:00Z"
"syncedAt": "2026-06-24T03:56:08Z"
}
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作",
"version": "20260605",
"keySource": "none",
"syncedAt": "2026-06-22T16:01:37Z"
"syncedAt": "2026-06-24T03:57:12Z"
}
@@ -1,14 +1,14 @@
{
"name": "next-skills",
"version": "0.1.0",
"description": "Next.js Agent Skills 提供 Next.js 最佳实践、版本升级和 Cache Components 工作流。",
"version": "0.2.0",
"description": "Next.js Agent Skills 提供 Cache Components 迁移、优化和 dev loop 验证工作流。",
"author": {
"name": "Vercel Labs",
"url": "https://github.com/vercel-labs"
"name": "Vercel",
"url": "https://github.com/vercel"
},
"homepage": "https://github.com/vercel-labs/next-skills",
"repository": "https://github.com/vercel-labs/next-skills",
"license": "UNKNOWN",
"homepage": "https://github.com/vercel/next.js/tree/canary/skills",
"repository": "https://github.com/vercel/next.js",
"license": "MIT",
"keywords": [
"nextjs",
"next-skills",
@@ -20,21 +20,21 @@
"skills": "./skills/",
"interface": {
"displayName": "Next.js Agent Skills",
"shortDescription": "Next.js 最佳实践、升级和 Cache Components 工作流。",
"longDescription": "Next.js Agent Skills 包含 next-best-practices、next-upgrade 和 next-cache-components 三个技能,覆盖 App Router 文件约定、RSC 边界、数据模式、异步 API、metadata、route handlers、图片/字体优化、打包、调试、Next.js 版本升级以及 Next.js 16 Cache Components。",
"developerName": "Vercel Labs",
"shortDescription": "Next.js Cache Components 迁移、优化和 dev loop 验证工作流。",
"longDescription": "Next.js Agent Skills 现在跟随 Next.js 仓库 canary/skills 版本,包含 next-cache-components-adoption、next-cache-components-optimizer 和 next-dev-loop 三个技能,覆盖 Cache Components 启用迁移、静态壳/即时导航优化,以及通过 Next.js dev MCP 与真实浏览器验证运行时行为。",
"developerName": "Vercel",
"category": "开发工具",
"capabilities": [
"Read",
"Write"
],
"defaultPrompt": [
"使用 Next.js Agent Skills 帮我检查这个 Next.js 项目。",
"使用 Next.js Agent Skills 帮我升级 Next.js 或处理 Cache Components。"
"使用 Next.js Agent Skills 帮我启用或优化 Cache Components。",
"使用 Next.js Agent Skills 帮我通过 dev loop 验证 Next.js 运行时行为。"
],
"websiteURL": "https://github.com/vercel-labs/next-skills",
"privacyPolicyURL": "https://github.com/vercel-labs/next-skills",
"termsOfServiceURL": "https://github.com/vercel-labs/next-skills",
"websiteURL": "https://github.com/vercel/next.js/tree/canary/skills",
"privacyPolicyURL": "https://github.com/vercel/next.js",
"termsOfServiceURL": "https://github.com/vercel/next.js",
"brandColor": "#000000",
"screenshots": []
}
@@ -1,9 +1,9 @@
{
"sourceId": "next-skills",
"repo": "https://github.com/vercel-labs/next-skills.git",
"ref": "main",
"commit": "dc1de9caf7612d73f56a8dec3cb1bd6c9ec096b9",
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"commit": "16db9d05442fc1521c02e6690599aa89da0df0ec",
"adapter": "skill-collection",
"sourcePath": "skills",
"syncedAt": "2026-06-15T08:24:34Z"
"syncedAt": "2026-06-24T03:48:19Z"
}
@@ -1,153 +0,0 @@
---
name: next-best-practices
description: "Next.js 最佳实践背景知识,覆盖文件约定、RSC 边界、数据模式、异步 API、metadata、route handlers、图片/字体优化、打包和调试。"
user-invocable: false
---
# Next.js Best Practices
Apply these rules when writing or reviewing Next.js code.
## File Conventions
See [file-conventions.md](./file-conventions.md) for:
- Project structure and special files
- Route segments (dynamic, catch-all, groups)
- Parallel and intercepting routes
- Middleware rename in v16 (middleware → proxy)
## RSC Boundaries
Detect invalid React Server Component patterns.
See [rsc-boundaries.md](./rsc-boundaries.md) for:
- Async client component detection (invalid)
- Non-serializable props detection
- Server Action exceptions
## Async Patterns
Next.js 15+ async API changes.
See [async-patterns.md](./async-patterns.md) for:
- Async `params` and `searchParams`
- Async `cookies()` and `headers()`
- Migration codemod
## Runtime Selection
See [runtime-selection.md](./runtime-selection.md) for:
- Default to Node.js runtime
- When Edge runtime is appropriate
## Directives
See [directives.md](./directives.md) for:
- `'use client'`, `'use server'` (React)
- `'use cache'` (Next.js)
## Functions
See [functions.md](./functions.md) for:
- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`
- Server functions: `cookies`, `headers`, `draftMode`, `after`
- Generate functions: `generateStaticParams`, `generateMetadata`
## Error Handling
See [error-handling.md](./error-handling.md) for:
- `error.tsx`, `global-error.tsx`, `not-found.tsx`
- `redirect`, `permanentRedirect`, `notFound`
- `forbidden`, `unauthorized` (auth errors)
- `unstable_rethrow` for catch blocks
## Data Patterns
See [data-patterns.md](./data-patterns.md) for:
- Server Components vs Server Actions vs Route Handlers
- Avoiding data waterfalls (`Promise.all`, Suspense, preload)
- Client component data fetching
## Route Handlers
See [route-handlers.md](./route-handlers.md) for:
- `route.ts` basics
- GET handler conflicts with `page.tsx`
- Environment behavior (no React DOM)
- When to use vs Server Actions
## Metadata & OG Images
See [metadata.md](./metadata.md) for:
- Static and dynamic metadata
- `generateMetadata` function
- OG image generation with `next/og`
- File-based metadata conventions
## Image Optimization
See [image.md](./image.md) for:
- Always use `next/image` over `<img>`
- Remote images configuration
- Responsive `sizes` attribute
- Blur placeholders
- Priority loading for LCP
## Font Optimization
See [font.md](./font.md) for:
- `next/font` setup
- Google Fonts, local fonts
- Tailwind CSS integration
- Preloading subsets
## Bundling
See [bundling.md](./bundling.md) for:
- Server-incompatible packages
- CSS imports (not link tags)
- Polyfills (already included)
- ESM/CommonJS issues
- Bundle analysis
## Scripts
See [scripts.md](./scripts.md) for:
- `next/script` vs native script tags
- Inline scripts need `id`
- Loading strategies
- Google Analytics with `@next/third-parties`
## Hydration Errors
See [hydration-error.md](./hydration-error.md) for:
- Common causes (browser APIs, dates, invalid HTML)
- Debugging with error overlay
- Fixes for each cause
## Suspense Boundaries
See [suspense-boundaries.md](./suspense-boundaries.md) for:
- CSR bailout with `useSearchParams` and `usePathname`
- Which hooks require Suspense boundaries
## Parallel & Intercepting Routes
See [parallel-routes.md](./parallel-routes.md) for:
- Modal patterns with `@slot` and `(.)` interceptors
- `default.tsx` for fallbacks
- Closing modals correctly with `router.back()`
## Self-Hosting
See [self-hosting.md](./self-hosting.md) for:
- `output: 'standalone'` for Docker
- Cache handlers for multi-instance ISR
- What works vs needs extra setup
## Debug Tricks
See [debug-tricks.md](./debug-tricks.md) for:
- MCP endpoint for AI-assisted debugging
- Rebuild specific routes with `--debug-build-paths`
@@ -1,87 +0,0 @@
# Async Patterns
In Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous.
## Async Params and SearchParams
Always type them as `Promise<...>` and await them.
### Pages and Layouts
```tsx
type Props = { params: Promise<{ slug: string }> }
export default async function Page({ params }: Props) {
const { slug } = await params
}
```
### Route Handlers
```tsx
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
}
```
### SearchParams
```tsx
type Props = {
params: Promise<{ slug: string }>
searchParams: Promise<{ query?: string }>
}
export default async function Page({ params, searchParams }: Props) {
const { slug } = await params
const { query } = await searchParams
}
```
### Synchronous Components
Use `React.use()` for non-async components:
```tsx
import { use } from 'react'
type Props = { params: Promise<{ slug: string }> }
export default function Page({ params }: Props) {
const { slug } = use(params)
}
```
### generateMetadata
```tsx
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
return { title: slug }
}
```
## Async Cookies and Headers
```tsx
import { cookies, headers } from 'next/headers'
export default async function Page() {
const cookieStore = await cookies()
const headersList = await headers()
const theme = cookieStore.get('theme')
const userAgent = headersList.get('user-agent')
}
```
## Migration Codemod
```bash
npx @next/codemod@latest next-async-request-api .
```
@@ -1,180 +0,0 @@
# Bundling
Fix common bundling issues with third-party packages.
## Server-Incompatible Packages
Some packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components.
### Error Signs
```
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined
Module not found: Can't resolve 'fs'
```
### Solution 1: Mark as Client-Only
If the package is only needed on client:
```tsx
// Bad: Fails - package uses window
import SomeChart from 'some-chart-library'
export default function Page() {
return <SomeChart />
}
// Good: Use dynamic import with ssr: false
import dynamic from 'next/dynamic'
const SomeChart = dynamic(() => import('some-chart-library'), {
ssr: false,
})
export default function Page() {
return <SomeChart />
}
```
### Solution 2: Externalize from Server Bundle
For packages that should run on server but have bundling issues:
```js
// next.config.js
module.exports = {
serverExternalPackages: ['problematic-package'],
}
```
Use this for:
- Packages with native bindings (sharp, bcrypt)
- Packages that don't bundle well (some ORMs)
- Packages with circular dependencies
### Solution 3: Client Component Wrapper
Wrap the entire usage in a client component:
```tsx
// components/ChartWrapper.tsx
'use client'
import { Chart } from 'chart-library'
export function ChartWrapper(props) {
return <Chart {...props} />
}
// app/page.tsx (server component)
import { ChartWrapper } from '@/components/ChartWrapper'
export default function Page() {
return <ChartWrapper data={data} />
}
```
## CSS Imports
Import CSS files instead of using `<link>` tags. Next.js handles bundling and optimization.
```tsx
// Bad: Manual link tag
<link rel="stylesheet" href="/styles.css" />
// Good: Import CSS
import './styles.css'
// Good: CSS Modules
import styles from './Button.module.css'
```
## Polyfills
Next.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs.
Already included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others.
```tsx
// Bad: Redundant polyfills
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from" />
// Good: Next.js includes these automatically
```
## ESM/CommonJS Issues
### Error Signs
```
SyntaxError: Cannot use import statement outside a module
Error: require() of ES Module
Module not found: ESM packages need to be imported
```
### Solution: Transpile Package
```js
// next.config.js
module.exports = {
transpilePackages: ['some-esm-package', 'another-package'],
}
```
## Common Problematic Packages
| Package | Issue | Solution |
|---------|-------|----------|
| `sharp` | Native bindings | `serverExternalPackages: ['sharp']` |
| `bcrypt` | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs` |
| `canvas` | Native bindings | `serverExternalPackages: ['canvas']` |
| `recharts` | Uses window | `dynamic(() => import('recharts'), { ssr: false })` |
| `react-quill` | Uses document | `dynamic(() => import('react-quill'), { ssr: false })` |
| `mapbox-gl` | Uses window | `dynamic(() => import('mapbox-gl'), { ssr: false })` |
| `monaco-editor` | Uses window | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |
| `lottie-web` | Uses document | `dynamic(() => import('lottie-react'), { ssr: false })` |
## Bundle Analysis
Analyze bundle size with the built-in analyzer (Next.js 16.1+):
```bash
next experimental-analyze
```
This opens an interactive UI to:
- Filter by route, environment (client/server), and type
- Inspect module sizes and import chains
- View treemap visualization
Save output for comparison:
```bash
next experimental-analyze --output
# Output saved to .next/diagnostics/analyze
```
Reference: https://nextjs.org/docs/app/guides/package-bundling
## Migrating from Webpack to Turbopack
Turbopack is the default bundler in Next.js 15+. If you have custom webpack config, migrate to Turbopack-compatible alternatives:
```js
// next.config.js
module.exports = {
// Good: Works with Turbopack
serverExternalPackages: ['package'],
transpilePackages: ['package'],
// Bad: Webpack-only - migrate away from this
webpack: (config) => {
// custom webpack config
},
}
```
Reference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack
@@ -1,297 +0,0 @@
# Data Patterns
Choose the right data fetching pattern for each use case.
## Decision Tree
```
Need to fetch data?
├── From a Server Component?
│ └── Use: Fetch directly (no API needed)
├── From a Client Component?
│ ├── Is it a mutation (POST/PUT/DELETE)?
│ │ └── Use: Server Action
│ └── Is it a read (GET)?
│ └── Use: Route Handler OR pass from Server Component
├── Need external API access (webhooks, third parties)?
│ └── Use: Route Handler
└── Need REST API for mobile app / external clients?
└── Use: Route Handler
```
## Pattern 1: Server Components (Preferred for Reads)
Fetch data directly in Server Components - no API layer needed.
```tsx
// app/users/page.tsx
async function UsersPage() {
// Direct database access - no API round-trip
const users = await db.user.findMany();
// Or fetch from external API
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
```
**Benefits**:
- No API to maintain
- No client-server waterfall
- Secrets stay on server
- Direct database access
## Pattern 2: Server Actions (Preferred for Mutations)
Server Actions are the recommended way to handle mutations.
```tsx
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts');
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidateTag('posts');
}
```
```tsx
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
```
**Benefits**:
- End-to-end type safety
- Progressive enhancement (works without JS)
- Automatic request handling
- Integrated with React transitions
**Constraints**:
- POST only (no GET caching semantics)
- Internal use only (no external access)
- Cannot return non-serializable data
## Pattern 3: Route Handlers (APIs)
Use Route Handlers when you need a REST API.
```tsx
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET is cacheable
export async function GET(request: NextRequest) {
const posts = await db.post.findMany();
return NextResponse.json(posts);
}
// POST for mutations
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
```
**When to use**:
- External API access (mobile apps, third parties)
- Webhooks from external services
- GET endpoints that need HTTP caching
- OpenAPI/Swagger documentation needed
**When NOT to use**:
- Internal data fetching (use Server Components)
- Mutations from your UI (use Server Actions)
## Avoiding Data Waterfalls
### Problem: Sequential Fetches
```tsx
// Bad: Sequential waterfalls
async function Dashboard() {
const user = await getUser(); // Wait...
const posts = await getPosts(); // Then wait...
const comments = await getComments(); // Then wait...
return <div>...</div>;
}
```
### Solution 1: Parallel Fetching with Promise.all
```tsx
// Good: Parallel fetching
async function Dashboard() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);
return <div>...</div>;
}
```
### Solution 2: Streaming with Suspense
```tsx
// Good: Show content progressively
import { Suspense } from 'react';
async function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserSection />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsSection />
</Suspense>
</div>
);
}
async function UserSection() {
const user = await getUser(); // Fetches independently
return <div>{user.name}</div>;
}
async function PostsSection() {
const posts = await getPosts(); // Fetches independently
return <PostList posts={posts} />;
}
```
### Solution 3: Preload Pattern
```tsx
// lib/data.ts
import { cache } from 'react';
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
export const preloadUser = (id: string) => {
void getUser(id); // Fire and forget
};
```
```tsx
// app/user/[id]/page.tsx
import { getUser, preloadUser } from '@/lib/data';
export default async function UserPage({ params }) {
const { id } = await params;
// Start fetching early
preloadUser(id);
// Do other work...
// Data likely ready by now
const user = await getUser(id);
return <div>{user.name}</div>;
}
```
## Client Component Data Fetching
When Client Components need data:
### Option 1: Pass from Server Component (Preferred)
```tsx
// Server Component
async function Page() {
const data = await fetchData();
return <ClientComponent initialData={data} />;
}
// Client Component
'use client';
function ClientComponent({ initialData }) {
const [data, setData] = useState(initialData);
// ...
}
```
### Option 2: Fetch on Mount (When Necessary)
```tsx
'use client';
import { useEffect, useState } from 'react';
function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(r => r.json())
.then(setData);
}, []);
if (!data) return <Loading />;
return <div>{data.value}</div>;
}
```
### Option 3: Server Action for Reads (Works But Not Ideal)
Server Actions can be called from Client Components for reads, but this is not their intended purpose:
```tsx
'use client';
import { getData } from './actions';
import { useEffect, useState } from 'react';
function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
getData().then(setData);
}, []);
return <div>{data?.value}</div>;
}
```
**Note**: Server Actions always use POST, so no HTTP caching. Prefer Route Handlers for cacheable reads.
## Quick Reference
| Pattern | Use Case | HTTP Method | Caching |
|---------|----------|-------------|---------|
| Server Component fetch | Internal reads | Any | Full Next.js caching |
| Server Action | Mutations, form submissions | POST only | No |
| Route Handler | External APIs, webhooks | Any | GET can be cached |
| Client fetch to API | Client-side reads | Any | HTTP cache headers |
@@ -1,105 +0,0 @@
# Debug Tricks
Tricks to speed up debugging Next.js applications.
## MCP Endpoint (Dev Server)
Next.js exposes a `/_next/mcp` endpoint in development for AI-assisted debugging via MCP (Model Context Protocol).
- **Next.js 16+**: Enabled by default, use `next-devtools-mcp`
- **Next.js < 16**: Requires `experimental.mcpServer: true` in next.config.js
Reference: https://nextjs.org/docs/app/guides/mcp
**Important**: Find the actual port of the running Next.js dev server (check terminal output or `package.json` scripts). Don't assume port 3000.
### Request Format
The endpoint uses JSON-RPC 2.0 over HTTP POST:
```bash
curl -X POST http://localhost:<port>/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "<tool-name>",
"arguments": {}
}
}'
```
### Available Tools
#### `get_errors`
Get current errors from dev server (build errors, runtime errors with source-mapped stacks):
```json
{ "name": "get_errors", "arguments": {} }
```
#### `get_routes`
Discover all routes by scanning filesystem:
```json
{ "name": "get_routes", "arguments": {} }
// Optional: { "name": "get_routes", "arguments": { "routerType": "app" } }
```
Returns: `{ "appRouter": ["/", "/api/users/[id]", ...], "pagesRouter": [...] }`
#### `get_project_metadata`
Get project path and dev server URL:
```json
{ "name": "get_project_metadata", "arguments": {} }
```
Returns: `{ "projectPath": "/path/to/project", "devServerUrl": "http://localhost:3000" }`
#### `get_page_metadata`
Get runtime metadata about current page render (requires active browser session):
```json
{ "name": "get_page_metadata", "arguments": {} }
```
Returns segment trie data showing layouts, boundaries, and page components.
#### `get_logs`
Get path to Next.js development log file:
```json
{ "name": "get_logs", "arguments": {} }
```
Returns path to `<distDir>/logs/next-development.log`
#### `get_server_action_by_id`
Locate a Server Action by ID:
```json
{ "name": "get_server_action_by_id", "arguments": { "actionId": "<action-id>" } }
```
### Example: Get Errors
```bash
curl -X POST http://localhost:<port>/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"get_errors","arguments":{}}}'
```
## Rebuild Specific Routes (Next.js 16+)
Use `--debug-build-paths` to rebuild only specific routes instead of the entire app:
```bash
# Rebuild a specific route
next build --debug-build-paths "/dashboard"
# Rebuild routes matching a glob
next build --debug-build-paths "/api/*"
# Dynamic routes
next build --debug-build-paths "/blog/[slug]"
```
Use this to:
- Quickly verify a build fix without full rebuild
- Debug static generation issues for specific pages
- Iterate faster on build errors
@@ -1,73 +0,0 @@
# Directives
## React Directives
These are React directives, not Next.js specific.
### `'use client'`
Marks a component as a Client Component. Required for:
- React hooks (`useState`, `useEffect`, etc.)
- Event handlers (`onClick`, `onChange`)
- Browser APIs (`window`, `localStorage`)
```tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
```
Reference: https://react.dev/reference/rsc/use-client
### `'use server'`
Marks a function as a Server Action. Can be passed to Client Components.
```tsx
'use server'
export async function submitForm(formData: FormData) {
// Runs on server
}
```
Or inline within a Server Component:
```tsx
export default function Page() {
async function submit() {
'use server'
// Runs on server
}
return <form action={submit}>...</form>
}
```
Reference: https://react.dev/reference/rsc/use-server
---
## Next.js Directive
### `'use cache'`
Marks a function or component for caching. Part of Next.js Cache Components.
```tsx
'use cache'
export async function getCachedData() {
return await fetchData()
}
```
Requires `cacheComponents: true` in `next.config.ts`.
For detailed usage including cache profiles, `cacheLife()`, `cacheTag()`, and `updateTag()`, see the `next-cache-components` skill.
Reference: https://nextjs.org/docs/app/api-reference/directives/use-cache
@@ -1,227 +0,0 @@
# Error Handling
Handle errors gracefully in Next.js applications.
Reference: https://nextjs.org/docs/app/getting-started/error-handling
## Error Boundaries
### `error.tsx`
Catches errors in a route segment and its children:
```tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
```
**Important:** `error.tsx` must be a Client Component.
### `global-error.tsx`
Catches errors in root layout:
```tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
```
**Important:** Must include `<html>` and `<body>` tags.
## Server Actions: Navigation API Gotcha
**Do NOT wrap navigation APIs in try-catch.** They throw special errors that Next.js handles internally.
Reference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior
```tsx
'use server'
import { redirect } from 'next/navigation'
import { notFound } from 'next/navigation'
// Bad: try-catch catches the navigation "error"
async function createPost(formData: FormData) {
try {
const post = await db.post.create({ ... })
redirect(`/posts/${post.id}`) // This throws!
} catch (error) {
// redirect() throw is caught here - navigation fails!
return { error: 'Failed to create post' }
}
}
// Good: Call navigation APIs outside try-catch
async function createPost(formData: FormData) {
let post
try {
post = await db.post.create({ ... })
} catch (error) {
return { error: 'Failed to create post' }
}
redirect(`/posts/${post.id}`) // Outside try-catch
}
// Good: Re-throw navigation errors
async function createPost(formData: FormData) {
try {
const post = await db.post.create({ ... })
redirect(`/posts/${post.id}`)
} catch (error) {
if (error instanceof Error && error.message === 'NEXT_REDIRECT') {
throw error // Re-throw navigation errors
}
return { error: 'Failed to create post' }
}
}
```
Same applies to:
- `redirect()` - 307 temporary redirect
- `permanentRedirect()` - 308 permanent redirect
- `notFound()` - 404 not found
- `forbidden()` - 403 forbidden
- `unauthorized()` - 401 unauthorized
Use `unstable_rethrow()` to re-throw these errors in catch blocks:
```tsx
import { unstable_rethrow } from 'next/navigation'
async function action() {
try {
// ...
redirect('/success')
} catch (error) {
unstable_rethrow(error) // Re-throws Next.js internal errors
return { error: 'Something went wrong' }
}
}
```
## Redirects
```tsx
import { redirect, permanentRedirect } from 'next/navigation'
// 307 Temporary - use for most cases
redirect('/new-path')
// 308 Permanent - use for URL migrations (cached by browsers)
permanentRedirect('/new-url')
```
## Auth Errors
Trigger auth-related error pages:
```tsx
import { forbidden, unauthorized } from 'next/navigation'
async function Page() {
const session = await getSession()
if (!session) {
unauthorized() // Renders unauthorized.tsx (401)
}
if (!session.hasAccess) {
forbidden() // Renders forbidden.tsx (403)
}
return <Dashboard />
}
```
Create corresponding error pages:
```tsx
// app/forbidden.tsx
export default function Forbidden() {
return <div>You don't have access to this resource</div>
}
// app/unauthorized.tsx
export default function Unauthorized() {
return <div>Please log in to continue</div>
}
```
## Not Found
### `not-found.tsx`
Custom 404 page for a route segment:
```tsx
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find the requested resource</p>
</div>
)
}
```
### Triggering Not Found
```tsx
import { notFound } from 'next/navigation'
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await getPost(id)
if (!post) {
notFound() // Renders closest not-found.tsx
}
return <div>{post.title}</div>
}
```
## Error Hierarchy
Errors bubble up to the nearest error boundary:
```
app/
├── error.tsx # Catches errors from all children
├── blog/
│ ├── error.tsx # Catches errors in /blog/*
│ └── [slug]/
│ ├── error.tsx # Catches errors in /blog/[slug]
│ └── page.tsx
└── layout.tsx # Errors here go to global-error.tsx
```
@@ -1,140 +0,0 @@
# File Conventions
Next.js App Router uses file-based routing with special file conventions.
## Project Structure
Reference: https://nextjs.org/docs/app/getting-started/project-structure
```
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 UI
├── global-error.tsx # Global error UI
├── route.ts # API endpoint
├── template.tsx # Re-rendered layout
├── default.tsx # Parallel route fallback
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── (group)/ # Route group (no URL impact)
└── page.tsx
```
## Special Files
| File | Purpose |
|------|---------|
| `page.tsx` | UI for a route segment |
| `layout.tsx` | Shared UI for segment and children |
| `loading.tsx` | Loading UI (Suspense boundary) |
| `error.tsx` | Error UI (Error boundary) |
| `not-found.tsx` | 404 UI |
| `route.ts` | API endpoint |
| `template.tsx` | Like layout but re-renders on navigation |
| `default.tsx` | Fallback for parallel routes |
## Route Segments
```
app/
├── blog/ # Static segment: /blog
├── [slug]/ # Dynamic segment: /:slug
├── [...slug]/ # Catch-all: /a/b/c
├── [[...slug]]/ # Optional catch-all: / or /a/b/c
└── (marketing)/ # Route group (ignored in URL)
```
## Parallel Routes
```
app/
├── @analytics/
│ └── page.tsx
├── @sidebar/
│ └── page.tsx
└── layout.tsx # Receives { analytics, sidebar } as props
```
## Intercepting Routes
```
app/
├── feed/
│ └── page.tsx
├── @modal/
│ └── (.)photo/[id]/ # Intercepts /photo/[id] from /feed
│ └── page.tsx
└── photo/[id]/
└── page.tsx
```
Conventions:
- `(.)` - same level
- `(..)` - one level up
- `(..)(..)` - two levels up
- `(...)` - from root
## Private Folders
```
app/
├── _components/ # Private folder (not a route)
│ └── Button.tsx
└── page.tsx
```
Prefix with `_` to exclude from routing.
## Middleware / Proxy
### Next.js 14-15: `middleware.ts`
```ts
// middleware.ts (root of project)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Auth, redirects, rewrites, etc.
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
```
### Next.js 16+: `proxy.ts`
Renamed for clarity - same capabilities, different names:
```ts
// proxy.ts (root of project)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
// Same logic as middleware
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
```
| Version | File | Export | Config |
|---------|------|--------|--------|
| v14-15 | `middleware.ts` | `middleware()` | `config` |
| v16+ | `proxy.ts` | `proxy()` | `config` |
**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.
## File Conventions Reference
Reference: https://nextjs.org/docs/app/api-reference/file-conventions
@@ -1,245 +0,0 @@
# Font Optimization
Use `next/font` for automatic font optimization with zero layout shift.
## Google Fonts
```tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
```
## Multiple Fonts
```tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
```
Use in CSS:
```css
body {
font-family: var(--font-inter);
}
code {
font-family: var(--font-roboto-mono);
}
```
## Font Weights and Styles
```tsx
// Single weight
const inter = Inter({
subsets: ['latin'],
weight: '400',
})
// Multiple weights
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '700'],
})
// Variable font (recommended) - includes all weights
const inter = Inter({
subsets: ['latin'],
// No weight needed - variable fonts support all weights
})
// With italic
const inter = Inter({
subsets: ['latin'],
style: ['normal', 'italic'],
})
```
## Local Fonts
```tsx
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/MyFont.woff2',
})
// Multiple files for different weights
const myFont = localFont({
src: [
{
path: './fonts/MyFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: './fonts/MyFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
})
// Variable font
const myFont = localFont({
src: './fonts/MyFont-Variable.woff2',
variable: '--font-my-font',
})
```
## Tailwind CSS Integration
```tsx
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
)
}
```
```js
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
},
},
},
}
```
## Preloading Subsets
Only load needed character subsets:
```tsx
// Latin only (most common)
const inter = Inter({ subsets: ['latin'] })
// Multiple subsets
const inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] })
```
## Display Strategy
Control font loading behavior:
```tsx
const inter = Inter({
subsets: ['latin'],
display: 'swap', // Default - shows fallback, swaps when loaded
})
// Options:
// 'auto' - browser decides
// 'block' - short block period, then swap
// 'swap' - immediate fallback, swap when ready (recommended)
// 'fallback' - short block, short swap, then fallback
// 'optional' - short block, no swap (use if font is optional)
```
## Don't Use Manual Font Links
Always use `next/font` instead of `<link>` tags for Google Fonts.
```tsx
// Bad: Manual link tag (blocks rendering, no optimization)
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
// Bad: Missing display and preconnect
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />
// Good: Use next/font (self-hosted, zero layout shift)
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
```
## Common Mistakes
```tsx
// Bad: Importing font in every component
// components/Button.tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] }) // Creates new instance each time!
// Good: Import once in layout, use CSS variable
// app/layout.tsx
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
// Bad: Using @import in CSS (blocks rendering)
/* globals.css */
@import url('https://fonts.googleapis.com/css2?family=Inter');
// Good: Use next/font (self-hosted, no network request)
import { Inter } from 'next/font/google'
// Bad: Loading all weights when only using a few
const inter = Inter({ subsets: ['latin'] }) // Loads all weights
// Good: Specify only needed weights (for non-variable fonts)
const inter = Inter({ subsets: ['latin'], weight: ['400', '700'] })
// Bad: Missing subset - loads all characters
const inter = Inter({})
// Good: Always specify subset
const inter = Inter({ subsets: ['latin'] })
```
## Font in Specific Components
```tsx
// For component-specific fonts, export from a shared file
// lib/fonts.ts
import { Inter, Playfair_Display } from 'next/font/google'
export const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' })
// components/Heading.tsx
import { playfair } from '@/lib/fonts'
export function Heading({ children }) {
return <h1 className={playfair.className}>{children}</h1>
}
```
@@ -1,108 +0,0 @@
# Functions
Next.js function APIs.
Reference: https://nextjs.org/docs/app/api-reference/functions
## Navigation Hooks (Client)
| Hook | Purpose | Reference |
|------|---------|-----------|
| `useRouter` | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router) |
| `usePathname` | Get current pathname | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname) |
| `useSearchParams` | Read URL search parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params) |
| `useParams` | Access dynamic route parameters | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params) |
| `useSelectedLayoutSegment` | Active child segment (one level) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment) |
| `useSelectedLayoutSegments` | All active segments below layout | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |
| `useLinkStatus` | Check link prefetch status | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status) |
| `useReportWebVitals` | Report Core Web Vitals metrics | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals) |
## Server Functions
| Function | Purpose | Reference |
|----------|---------|-----------|
| `cookies` | Read/write cookies | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies) |
| `headers` | Read request headers | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers) |
| `draftMode` | Enable preview of unpublished CMS content | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |
| `after` | Run code after response finishes streaming | [Docs](https://nextjs.org/docs/app/api-reference/functions/after) |
| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |
| `userAgent` | Parse User-Agent header | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent) |
## Generate Functions
| Function | Purpose | Reference |
|----------|---------|-----------|
| `generateStaticParams` | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params) |
| `generateMetadata` | Dynamic metadata | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) |
| `generateViewport` | Dynamic viewport config | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport) |
| `generateSitemaps` | Multiple sitemaps for large sites | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps) |
| `generateImageMetadata` | Multiple OG images per route | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |
## Request/Response
| Function | Purpose | Reference |
|----------|---------|-----------|
| `NextRequest` | Extended Request with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request) |
| `NextResponse` | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response) |
| `ImageResponse` | Generate OG images | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |
## Common Examples
### Navigation
Use `next/link` for internal navigation instead of `<a>` tags.
```tsx
// Bad: Plain anchor tag
<a href="/about">About</a>
// Good: Next.js Link
import Link from 'next/link'
<Link href="/about">About</Link>
```
Active link styling:
```tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export function NavLink({ href, children }) {
const pathname = usePathname()
return (
<Link href={href} className={pathname === href ? 'active' : ''}>
{children}
</Link>
)
}
```
### Static Generation
```tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
```
### After Response
```tsx
import { after } from 'next/server'
export async function POST(request: Request) {
const data = await processRequest(request)
after(async () => {
await logAnalytics(data)
})
return Response.json({ success: true })
}
```
@@ -1,91 +0,0 @@
# Hydration Errors
Diagnose and fix React hydration mismatch errors.
## Error Signs
- "Hydration failed because the initial UI does not match"
- "Text content does not match server-rendered HTML"
## Debugging
In development, click the hydration error to see the server/client diff.
## Common Causes and Fixes
### Browser-only APIs
```tsx
// Bad: Causes mismatch - window doesn't exist on server
<div>{window.innerWidth}</div>
// Good: Use client component with mounted check
'use client'
import { useState, useEffect } from 'react'
export function ClientOnly({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return mounted ? children : null
}
```
### Date/Time Rendering
Server and client may be in different timezones:
```tsx
// Bad: Causes mismatch
<span>{new Date().toLocaleString()}</span>
// Good: Render on client only
'use client'
const [time, setTime] = useState<string>()
useEffect(() => setTime(new Date().toLocaleString()), [])
```
### Random Values or IDs
```tsx
// Bad: Random values differ between server and client
<div id={Math.random().toString()}>
// Good: Use useId hook
import { useId } from 'react'
function Input() {
const id = useId()
return <input id={id} />
}
```
### Invalid HTML Nesting
```tsx
// Bad: Invalid - div inside p
<p><div>Content</div></p>
// Bad: Invalid - p inside p
<p><p>Nested</p></p>
// Good: Valid nesting
<div><p>Content</p></div>
```
### Third-party Scripts
Scripts that modify DOM during hydration.
```tsx
// Good: Use next/script with afterInteractive
import Script from 'next/script'
export default function Page() {
return (
<Script
src="https://example.com/script.js"
strategy="afterInteractive"
/>
)
}
```
@@ -1,173 +0,0 @@
# Image Optimization
Use `next/image` for automatic image optimization.
## Always Use next/image
```tsx
// Bad: Avoid native img
<img src="/hero.png" alt="Hero" />
// Good: Use next/image
import Image from 'next/image'
<Image src="/hero.png" alt="Hero" width={800} height={400} />
```
## Required Props
Images need explicit dimensions to prevent layout shift:
```tsx
// Local images - dimensions inferred automatically
import heroImage from './hero.png'
<Image src={heroImage} alt="Hero" />
// Remote images - must specify width/height
<Image src="https://example.com/image.jpg" alt="Hero" width={800} height={400} />
// Or use fill for parent-relative sizing
<div style={{ position: 'relative', width: '100%', height: 400 }}>
<Image src="/hero.png" alt="Hero" fill style={{ objectFit: 'cover' }} />
</div>
```
## Remote Images Configuration
Remote domains must be configured in `next.config.js`:
```js
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: '*.cdn.com', // Wildcard subdomain
},
],
},
}
```
## Responsive Images
Use `sizes` to tell the browser which size to download:
```tsx
// Full-width hero
<Image
src="/hero.png"
alt="Hero"
fill
sizes="100vw"
/>
// Responsive grid (3 columns on desktop, 1 on mobile)
<Image
src="/card.png"
alt="Card"
fill
sizes="(max-width: 768px) 100vw, 33vw"
/>
// Fixed sidebar image
<Image
src="/avatar.png"
alt="Avatar"
width={200}
height={200}
sizes="200px"
/>
```
## Blur Placeholder
Prevent layout shift with placeholders:
```tsx
// Local images - automatic blur hash
import heroImage from './hero.png'
<Image src={heroImage} alt="Hero" placeholder="blur" />
// Remote images - provide blurDataURL
<Image
src="https://example.com/image.jpg"
alt="Hero"
width={800}
height={400}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
// Or use color placeholder
<Image
src="https://example.com/image.jpg"
alt="Hero"
width={800}
height={400}
placeholder="empty"
style={{ backgroundColor: '#e0e0e0' }}
/>
```
## Priority Loading
Use `priority` for above-the-fold images (LCP):
```tsx
// Hero image - loads immediately
<Image src="/hero.png" alt="Hero" fill priority />
// Below-fold images - lazy loaded by default (no priority needed)
<Image src="/card.png" alt="Card" width={400} height={300} />
```
## Common Mistakes
```tsx
// Bad: Missing sizes with fill - downloads largest image
<Image src="/hero.png" alt="Hero" fill />
// Good: Add sizes for proper responsive behavior
<Image src="/hero.png" alt="Hero" fill sizes="100vw" />
// Bad: Using width/height for aspect ratio only
<Image src="/hero.png" alt="Hero" width={16} height={9} />
// Good: Use actual display dimensions or fill with sizes
<Image src="/hero.png" alt="Hero" fill sizes="100vw" style={{ objectFit: 'cover' }} />
// Bad: Remote image without config
<Image src="https://untrusted.com/image.jpg" alt="Image" width={400} height={300} />
// Error: Invalid src prop, hostname not configured
// Good: Add hostname to next.config.js remotePatterns
```
## Static Export
When using `output: 'export'`, use `unoptimized` or custom loader:
```tsx
// Option 1: Disable optimization
<Image src="/hero.png" alt="Hero" width={800} height={400} unoptimized />
// Option 2: Global config
// next.config.js
module.exports = {
output: 'export',
images: { unoptimized: true },
}
// Option 3: Custom loader (Cloudinary, Imgix, etc.)
const cloudinaryLoader = ({ src, width, quality }) => {
return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`
}
<Image loader={cloudinaryLoader} src="sample.jpg" alt="Sample" width={800} height={400} />
```
@@ -1,301 +0,0 @@
# Metadata
Add SEO metadata to Next.js pages using the Metadata API.
## Important: Server Components Only
The `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.
If the target page has `'use client'`:
1. Remove `'use client'` if possible, move client logic to child components
2. Or extract metadata to a parent Server Component layout
3. Or split the file: Server Component with metadata imports Client Components
## Static Metadata
```tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Page Title',
description: 'Page description for search engines',
}
```
## Dynamic Metadata
```tsx
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title, description: post.description }
}
```
## Avoid Duplicate Fetches
Use React `cache()` when the same data is needed for both metadata and page:
```tsx
import { cache } from 'react'
export const getPost = cache(async (slug: string) => {
return await db.posts.findFirst({ where: { slug } })
})
```
## Viewport
Separate from metadata for streaming support:
```tsx
import type { Viewport } from 'next'
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#000000',
}
// Or dynamic
export function generateViewport({ params }): Viewport {
return { themeColor: getThemeColor(params) }
}
```
## Title Templates
In root layout for consistent naming:
```tsx
export const metadata: Metadata = {
title: { default: 'Site Name', template: '%s | Site Name' },
}
```
## Metadata File Conventions
Reference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions
Place these files in `app/` directory (or route segments):
| File | Purpose |
|------|---------|
| `favicon.ico` | Favicon |
| `icon.png` / `icon.svg` | App icon |
| `apple-icon.png` | Apple app icon |
| `opengraph-image.png` | OG image |
| `twitter-image.png` | Twitter card image |
| `sitemap.ts` / `sitemap.xml` | Sitemap (use `generateSitemaps` for multiple) |
| `robots.ts` / `robots.txt` | Robots directives |
| `manifest.ts` / `manifest.json` | Web app manifest |
## SEO Best Practice: Static Files Are Often Enough
For most sites, **static metadata files provide excellent SEO coverage**:
```
app/
├── favicon.ico
├── opengraph-image.png # Works for both OG and Twitter
├── sitemap.ts
├── robots.ts
└── layout.tsx # With title/description metadata
```
**Tips:**
- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)
- Static `title` and `description` in layout metadata is sufficient for most pages
- Only use dynamic `generateMetadata` when content varies per page
---
# OG Image Generation
Generate dynamic Open Graph images using `next/og`.
## Important Rules
1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)
2. **No searchParams** - OG images can't access search params, use route params instead
3. **Avoid Edge runtime** - Use default Node.js runtime
```tsx
// Good
import { ImageResponse } from 'next/og'
// Bad
// import { ImageResponse } from '@vercel/og'
// export const runtime = 'edge'
```
## Basic OG Image
```tsx
// app/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const alt = 'Site Name'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default function Image() {
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Hello World
</div>
),
{ ...size }
)
}
```
## Dynamic OG Image
```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const alt = 'Blog Post'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
type Props = { params: Promise<{ slug: string }> }
export default async function Image({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
(
<div
style={{
fontSize: 48,
background: 'linear-gradient(to bottom, #1a1a1a, #333)',
color: 'white',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 48,
}}
>
<div style={{ fontSize: 64, fontWeight: 'bold' }}>{post.title}</div>
<div style={{ marginTop: 24, opacity: 0.8 }}>{post.description}</div>
</div>
),
{ ...size }
)
}
```
## Custom Fonts
```tsx
import { ImageResponse } from 'next/og'
import { join } from 'path'
import { readFile } from 'fs/promises'
export default async function Image() {
const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf')
const fontData = await readFile(fontPath)
return new ImageResponse(
(
<div style={{ fontFamily: 'Inter', fontSize: 64 }}>
Custom Font Text
</div>
),
{
width: 1200,
height: 630,
fonts: [{ name: 'Inter', data: fontData, style: 'normal' }],
}
)
}
```
## File Naming
- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)
- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)
## Styling Notes
ImageResponse uses Flexbox layout:
- Use `display: 'flex'`
- No CSS Grid support
- Styles must be inline objects
## Multiple OG Images
Use `generateImageMetadata` for multiple images per route:
```tsx
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export async function generateImageMetadata({ params }) {
const images = await getPostImages(params.slug)
return images.map((img, idx) => ({
id: idx,
alt: img.alt,
size: { width: 1200, height: 630 },
contentType: 'image/png',
}))
}
export default async function Image({ params, id }) {
const images = await getPostImages(params.slug)
const image = images[id]
return new ImageResponse(/* ... */)
}
```
## Multiple Sitemaps
Use `generateSitemaps` for large sites:
```tsx
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export async function generateSitemaps() {
// Return array of sitemap IDs
return [{ id: 0 }, { id: 1 }, { id: 2 }]
}
export default async function sitemap({
id,
}: {
id: number
}): Promise<MetadataRoute.Sitemap> {
const start = id * 50000
const end = start + 50000
const products = await getProducts(start, end)
return products.map((product) => ({
url: `https://example.com/product/${product.id}`,
lastModified: product.updatedAt,
}))
}
```
Generates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.
@@ -1,287 +0,0 @@
# Parallel & Intercepting Routes
Parallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.
## File Structure
```
app/
├── @modal/ # Parallel route slot
│ ├── default.tsx # Required! Returns null
│ ├── (.)photos/ # Intercepts /photos/*
│ │ └── [id]/
│ │ └── page.tsx # Modal content
│ └── [...]catchall/ # Optional: catch unmatched
│ └── page.tsx
├── photos/
│ └── [id]/
│ └── page.tsx # Full page (direct access)
├── layout.tsx # Renders both children and @modal
└── page.tsx
```
## Step 1: Root Layout with Slot
```tsx
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
```
## Step 2: Default File (Critical!)
**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.
```tsx
// app/@modal/default.tsx
export default function Default() {
return null;
}
```
Without this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.
## Step 3: Intercepting Route (Modal)
The `(.)` prefix intercepts routes at the same level.
```tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/modal';
export default async function PhotoModal({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.title} />
</Modal>
);
}
```
## Step 4: Full Page (Direct Access)
```tsx
// app/photos/[id]/page.tsx
export default async function PhotoPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<div className="full-page">
<img src={photo.url} alt={photo.title} />
<h1>{photo.title}</h1>
</div>
);
}
```
## Step 5: Modal Component with Correct Closing
**Critical: Use `router.back()` to close modals, NOT `router.push()` or `<Link>`.**
```tsx
// components/modal.tsx
'use client';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef } from 'react';
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter();
const overlayRef = useRef<HTMLDivElement>(null);
// Close on escape key
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
router.back(); // Correct
}
}
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [router]);
// Close on overlay click
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
if (e.target === overlayRef.current) {
router.back(); // Correct
}
}, [router]);
return (
<div
ref={overlayRef}
onClick={handleOverlayClick}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
>
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
<button
onClick={() => router.back()} // Correct!
className="absolute top-4 right-4"
>
Close
</button>
{children}
</div>
</div>
);
}
```
### Why NOT `router.push('/')` or `<Link href="/">`?
Using `push` or `Link` to "close" a modal:
1. Adds a new history entry (back button shows modal again)
2. Doesn't properly clear the intercepted route
3. Can cause the modal to flash or persist unexpectedly
`router.back()` correctly:
1. Removes the intercepted route from history
2. Returns to the previous page
3. Properly unmounts the modal
## Route Matcher Reference
Matchers match **route segments**, not filesystem paths:
| Matcher | Matches | Example |
|---------|---------|---------|
| `(.)` | Same level | `@modal/(.)photos` intercepts `/photos` |
| `(..)` | One level up | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |
| `(..)(..)` | Two levels up | Rarely used |
| `(...)` | From root | `@modal/(...)photos` intercepts `/photos` from anywhere |
**Common mistake**: Thinking `(..)` means "parent folder" - it means "parent route segment".
## Handling Hard Navigation
When users directly visit `/photos/123` (bookmark, refresh, shared link):
- The intercepting route is bypassed
- The full `photos/[id]/page.tsx` renders
- Modal doesn't appear (expected behavior)
If you want the modal to appear on direct access too, you need additional logic:
```tsx
// app/photos/[id]/page.tsx
import { Modal } from '@/components/modal';
export default async function PhotoPage({ params }) {
const { id } = await params;
const photo = await getPhoto(id);
// Option: Render as modal on direct access too
return (
<Modal>
<img src={photo.url} alt={photo.title} />
</Modal>
);
}
```
## Common Gotchas
### 1. Missing `default.tsx` → 404 on Refresh
Every `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).
### 2. Modal Persists After Navigation
You're using `router.push()` instead of `router.back()`.
### 3. Nested Parallel Routes Need Defaults Too
If you have `@modal` inside a route group, each level needs its own `default.tsx`:
```
app/
├── (marketing)/
│ ├── @modal/
│ │ └── default.tsx # Needed!
│ └── layout.tsx
└── layout.tsx
```
### 4. Intercepted Route Shows Wrong Content
Check your matcher:
- `(.)photos` intercepts `/photos` from the same route level
- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`
### 5. TypeScript Errors with `params`
In Next.js 15+, `params` is a Promise:
```tsx
// Correct
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
}
```
## Complete Example: Photo Gallery Modal
```
app/
├── @modal/
│ ├── default.tsx
│ └── (.)photos/
│ └── [id]/
│ └── page.tsx
├── photos/
│ ├── page.tsx # Gallery grid
│ └── [id]/
│ └── page.tsx # Full photo page
├── layout.tsx
└── page.tsx
```
Links in the gallery:
```tsx
// app/photos/page.tsx
import Link from 'next/link';
export default async function Gallery() {
const photos = await getPhotos();
return (
<div className="grid grid-cols-3 gap-4">
{photos.map(photo => (
<Link key={photo.id} href={`/photos/${photo.id}`}>
<img src={photo.thumbnail} alt={photo.title} />
</Link>
))}
</div>
);
}
```
Clicking a photo → Modal opens (intercepted)
Direct URL → Full page renders
Refresh while modal open → Full page renders
@@ -1,146 +0,0 @@
# Route Handlers
Create API endpoints with `route.ts` files.
## Basic Usage
```tsx
// app/api/users/route.ts
export async function GET() {
const users = await getUsers()
return Response.json(users)
}
export async function POST(request: Request) {
const body = await request.json()
const user = await createUser(body)
return Response.json(user, { status: 201 })
}
```
## Supported Methods
`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`
## GET Handler Conflicts with page.tsx
**A `route.ts` and `page.tsx` cannot coexist in the same folder.**
```
app/
├── api/
│ └── users/
│ └── route.ts # /api/users
└── users/
├── page.tsx # /users (page)
└── route.ts # Warning: Conflicts with page.tsx!
```
If you need both a page and an API at the same path, use different paths:
```
app/
├── users/
│ └── page.tsx # /users (page)
└── api/
└── users/
└── route.ts # /api/users (API)
```
## Environment Behavior
Route handlers run in a **Server Component-like environment**:
- Yes: Can use `async/await`
- Yes: Can access `cookies()`, `headers()`
- Yes: Can use Node.js APIs
- No: Cannot use React hooks
- No: Cannot use React DOM APIs
- No: Cannot use browser APIs
```tsx
// Bad: This won't work - no React DOM in route handlers
import { renderToString } from 'react-dom/server'
export async function GET() {
const html = renderToString(<Component />) // Error!
return new Response(html)
}
```
## Dynamic Route Handlers
```tsx
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const user = await getUser(id)
if (!user) {
return Response.json({ error: 'Not found' }, { status: 404 })
}
return Response.json(user)
}
```
## Request Helpers
```tsx
export async function GET(request: Request) {
// URL and search params
const { searchParams } = new URL(request.url)
const query = searchParams.get('q')
// Headers
const authHeader = request.headers.get('authorization')
// Cookies (Next.js helper)
const cookieStore = await cookies()
const token = cookieStore.get('token')
return Response.json({ query, token })
}
```
## Response Helpers
```tsx
// JSON response
return Response.json({ data })
// With status
return Response.json({ error: 'Not found' }, { status: 404 })
// With headers
return Response.json(data, {
headers: {
'Cache-Control': 'max-age=3600',
},
})
// Redirect
return Response.redirect(new URL('/login', request.url))
// Stream
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
})
```
## When to Use Route Handlers vs Server Actions
| Use Case | Route Handlers | Server Actions |
|----------|----------------|----------------|
| Form submissions | No | Yes |
| Data mutations from UI | No | Yes |
| Third-party webhooks | Yes | No |
| External API consumption | Yes | No |
| Public REST API | Yes | No |
| File uploads | Both work | Both work |
**Prefer Server Actions** for mutations triggered from your UI.
**Use Route Handlers** for external integrations and public APIs.
@@ -1,159 +0,0 @@
# RSC Boundaries
Detect and prevent invalid patterns when crossing Server/Client component boundaries.
## Detection Rules
### 1. Async Client Components Are Invalid
Client components **cannot** be async functions. Only Server Components can be async.
**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`
```tsx
// Bad: async client component
'use client'
export default async function UserProfile() {
const user = await getUser() // Cannot await in client component
return <div>{user.name}</div>
}
// Good: Remove async, fetch data in parent server component
// page.tsx (server component - no 'use client')
export default async function Page() {
const user = await getUser()
return <UserProfile user={user} />
}
// UserProfile.tsx (client component)
'use client'
export function UserProfile({ user }: { user: User }) {
return <div>{user.name}</div>
}
```
```tsx
// Bad: async arrow function client component
'use client'
const Dashboard = async () => {
const data = await fetchDashboard()
return <div>{data}</div>
}
// Good: Fetch in server component, pass data down
```
### 2. Non-Serializable Props to Client Components
Props passed from Server → Client must be JSON-serializable.
**Detect:** Server component passes these to a client component:
- Functions (except Server Actions with `'use server'`)
- `Date` objects
- `Map`, `Set`, `WeakMap`, `WeakSet`
- Class instances
- `Symbol` (unless globally registered)
- Circular references
```tsx
// Bad: Function prop
// page.tsx (server)
export default function Page() {
const handleClick = () => console.log('clicked')
return <ClientButton onClick={handleClick} />
}
// Good: Define function inside client component
// ClientButton.tsx
'use client'
export function ClientButton() {
const handleClick = () => console.log('clicked')
return <button onClick={handleClick}>Click</button>
}
```
```tsx
// Bad: Date object (silently becomes string, then crashes)
// page.tsx (server)
export default async function Page() {
const post = await getPost()
return <PostCard createdAt={post.createdAt} /> // Date object
}
// PostCard.tsx (client) - will crash on .getFullYear()
'use client'
export function PostCard({ createdAt }: { createdAt: Date }) {
return <span>{createdAt.getFullYear()}</span> // Runtime error!
}
// Good: Serialize to string on server
// page.tsx (server)
export default async function Page() {
const post = await getPost()
return <PostCard createdAt={post.createdAt.toISOString()} />
}
// PostCard.tsx (client)
'use client'
export function PostCard({ createdAt }: { createdAt: string }) {
const date = new Date(createdAt)
return <span>{date.getFullYear()}</span>
}
```
```tsx
// Bad: Class instance
const user = new UserModel(data)
<ClientProfile user={user} /> // Methods will be stripped
// Good: Pass plain object
const user = await getUser()
<ClientProfile user={{ id: user.id, name: user.name }} />
```
```tsx
// Bad: Map/Set
<ClientComponent items={new Map([['a', 1]])} />
// Good: Convert to array/object
<ClientComponent items={Object.fromEntries(map)} />
<ClientComponent items={Array.from(set)} />
```
### 3. Server Actions Are the Exception
Functions marked with `'use server'` CAN be passed to client components.
```tsx
// Valid: Server Action can be passed
// actions.ts
'use server'
export async function submitForm(formData: FormData) {
// server-side logic
}
// page.tsx (server)
import { submitForm } from './actions'
export default function Page() {
return <ClientForm onSubmit={submitForm} /> // OK!
}
// ClientForm.tsx (client)
'use client'
export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
return <form action={onSubmit}>...</form>
}
```
## Quick Reference
| Pattern | Valid? | Fix |
|---------|--------|-----|
| `'use client'` + `async function` | No | Fetch in server parent, pass data |
| Pass `() => {}` to client | No | Define in client or use server action |
| Pass `new Date()` to client | No | Use `.toISOString()` |
| Pass `new Map()` to client | No | Convert to object/array |
| Pass class instance to client | No | Pass plain object |
| Pass server action to client | Yes | - |
| Pass `string/number/boolean` | Yes | - |
| Pass plain object/array | Yes | - |
@@ -1,39 +0,0 @@
# Runtime Selection
## Use Node.js Runtime by Default
Use the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.
```tsx
// Good: Default - no runtime config needed (uses Node.js)
export default function Page() { ... }
// Caution: Only if already used in project or specifically required
export const runtime = 'edge'
```
## When to Use Each
### Node.js Runtime (Default)
- Full Node.js API support
- File system access (`fs`)
- Full `crypto` support
- Database connections
- Most npm packages work
### Edge Runtime
- Only for specific edge-location latency requirements
- Limited API (no `fs`, limited `crypto`)
- Smaller cold start
- Geographic distribution needs
## Detection
**Before adding `runtime = 'edge'`**, check:
1. Does the project already use Edge runtime?
2. Is there a specific latency requirement?
3. Are all dependencies Edge-compatible?
If unsure, use Node.js runtime.
@@ -1,141 +0,0 @@
# Scripts
Loading third-party scripts in Next.js.
## Use next/script
Always use `next/script` instead of native `<script>` tags for better performance.
```tsx
// Bad: Native script tag
<script src="https://example.com/script.js"></script>
// Good: Next.js Script component
import Script from 'next/script'
<Script src="https://example.com/script.js" />
```
## Inline Scripts Need ID
Inline scripts require an `id` attribute for Next.js to track them.
```tsx
// Bad: Missing id
<Script dangerouslySetInnerHTML={{ __html: 'console.log("hi")' }} />
// Good: Has id
<Script id="my-script" dangerouslySetInnerHTML={{ __html: 'console.log("hi")' }} />
// Good: Inline with id
<Script id="show-banner">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>
```
## Don't Put Script in Head
`next/script` should not be placed inside `next/head`. It handles its own positioning.
```tsx
// Bad: Script inside Head
import Head from 'next/head'
import Script from 'next/script'
<Head>
<Script src="/analytics.js" />
</Head>
// Good: Script outside Head
<Head>
<title>Page</title>
</Head>
<Script src="/analytics.js" />
```
## Loading Strategies
```tsx
// afterInteractive (default) - Load after page is interactive
<Script src="/analytics.js" strategy="afterInteractive" />
// lazyOnload - Load during idle time
<Script src="/widget.js" strategy="lazyOnload" />
// beforeInteractive - Load before page is interactive (use sparingly)
// Only works in app/layout.tsx or pages/_document.js
<Script src="/critical.js" strategy="beforeInteractive" />
// worker - Load in web worker (experimental)
<Script src="/heavy.js" strategy="worker" />
```
## Google Analytics
Use `@next/third-parties` instead of inline GA scripts.
```tsx
// Bad: Inline GA script
<Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX" />
<Script id="ga-init">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXX');`}
</Script>
// Good: Next.js component
import { GoogleAnalytics } from '@next/third-parties/google'
export default function Layout({ children }) {
return (
<html>
<body>{children}</body>
<GoogleAnalytics gaId="G-XXXXX" />
</html>
)
}
```
## Google Tag Manager
```tsx
import { GoogleTagManager } from '@next/third-parties/google'
export default function Layout({ children }) {
return (
<html>
<GoogleTagManager gtmId="GTM-XXXXX" />
<body>{children}</body>
</html>
)
}
```
## Other Third-Party Scripts
```tsx
// YouTube embed
import { YouTubeEmbed } from '@next/third-parties/google'
<YouTubeEmbed videoid="dQw4w9WgXcQ" />
// Google Maps
import { GoogleMapsEmbed } from '@next/third-parties/google'
<GoogleMapsEmbed
apiKey="YOUR_API_KEY"
mode="place"
q="Brooklyn+Bridge,New+York,NY"
/>
```
## Quick Reference
| Pattern | Issue | Fix |
|---------|-------|-----|
| `<script src="...">` | No optimization | Use `next/script` |
| `<Script>` without id | Can't track inline scripts | Add `id` attribute |
| `<Script>` inside `<Head>` | Wrong placement | Move outside Head |
| Inline GA/GTM scripts | No optimization | Use `@next/third-parties` |
| `strategy="beforeInteractive"` outside layout | Won't work | Only use in root layout |
@@ -1,371 +0,0 @@
# Self-Hosting Next.js
Deploy Next.js outside of Vercel with confidence.
## Quick Start: Standalone Output
For Docker or any containerized deployment, use standalone output:
```js
// next.config.js
module.exports = {
output: 'standalone',
};
```
This creates a minimal `standalone` folder with only production dependencies:
```
.next/
├── standalone/
│ ├── server.js # Entry point
│ ├── node_modules/ # Only production deps
│ └── .next/ # Build output
└── static/ # Must be copied separately
```
## Docker Deployment
### Dockerfile
```dockerfile
FROM node:20-alpine AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
```
### Docker Compose
```yaml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
```
## PM2 Deployment
For traditional server deployments:
```js
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nextjs',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
}],
};
```
```bash
npm run build
pm2 start ecosystem.config.js
```
## ISR and Cache Handlers
### The Problem
ISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:
- Instance A regenerates page → saves to its local disk
- Instance B serves stale page → doesn't see Instance A's cache
- Load balancer sends users to random instances → inconsistent content
### Solution: Custom Cache Handler
Next.js 14+ supports custom cache handlers for shared storage:
```js
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // Disable in-memory cache
};
```
#### Redis Cache Handler Example
```js
// cache-handler.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const CACHE_PREFIX = 'nextjs:';
module.exports = class CacheHandler {
constructor(options) {
this.options = options;
}
async get(key) {
const data = await redis.get(CACHE_PREFIX + key);
if (!data) return null;
const parsed = JSON.parse(data);
return {
value: parsed.value,
lastModified: parsed.lastModified,
};
}
async set(key, data, ctx) {
const cacheData = {
value: data,
lastModified: Date.now(),
};
// Set TTL based on revalidate option
if (ctx?.revalidate) {
await redis.setex(
CACHE_PREFIX + key,
ctx.revalidate,
JSON.stringify(cacheData)
);
} else {
await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));
}
}
async revalidateTag(tags) {
// Implement tag-based invalidation
// This requires tracking which keys have which tags
}
};
```
#### S3 Cache Handler Example
```js
// cache-handler.js
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.CACHE_BUCKET;
module.exports = class CacheHandler {
async get(key) {
try {
const response = await s3.send(new GetObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
}));
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (err) {
if (err.name === 'NoSuchKey') return null;
throw err;
}
}
async set(key, data, ctx) {
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
Body: JSON.stringify({
value: data,
lastModified: Date.now(),
}),
ContentType: 'application/json',
}));
}
};
```
## What Works vs What Needs Setup
| Feature | Single Instance | Multi-Instance | Notes |
|---------|----------------|----------------|-------|
| SSR | Yes | Yes | No special setup |
| SSG | Yes | Yes | Built at deploy time |
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
| Middleware | Yes | Yes | Runs on Node.js |
| Edge Runtime | Limited | Limited | Some features Node-only |
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
| `next/font` | Yes | Yes | Fonts bundled at build |
| Draft Mode | Yes | Yes | Cookie-based |
## Image Optimization
Next.js Image Optimization works out of the box but is CPU-intensive.
### Option 1: Built-in (Simple)
Works automatically, but consider:
- Set `deviceSizes` and `imageSizes` in config to limit variants
- Use `minimumCacheTTL` to reduce regeneration
```js
// next.config.js
module.exports = {
images: {
minimumCacheTTL: 60 * 60 * 24, // 24 hours
deviceSizes: [640, 750, 1080, 1920], // Limit sizes
},
};
```
### Option 2: External Loader (Recommended for Scale)
Offload to Cloudinary, Imgix, or similar:
```js
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.js',
},
};
```
```js
// lib/image-loader.js
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;
}
```
## Environment Variables
### Build-time vs Runtime
```js
// Available at build time only (baked into bundle)
NEXT_PUBLIC_API_URL=https://api.example.com
// Available at runtime (server-side only)
DATABASE_URL=postgresql://...
API_SECRET=...
```
### Runtime Configuration
For truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:
```tsx
// app/api/config/route.ts
export async function GET() {
return Response.json({
apiUrl: process.env.API_URL,
features: process.env.FEATURES?.split(','),
});
}
```
## OpenNext: Serverless Without Vercel
[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.
```bash
npx create-sst@latest
# or
npx @opennextjs/aws build
```
Supports:
- AWS Lambda + CloudFront
- Cloudflare Workers
- Netlify Functions
- Deno Deploy
## Health Check Endpoint
Always include a health check for load balancers:
```tsx
// app/api/health/route.ts
export async function GET() {
try {
// Optional: check database connection
// await db.$queryRaw`SELECT 1`;
return Response.json({ status: 'healthy' }, { status: 200 });
} catch (error) {
return Response.json({ status: 'unhealthy' }, { status: 503 });
}
}
```
## Pre-Deployment Checklist
1. **Build locally first**: `npm run build` - catch errors before deploy
2. **Test standalone output**: `node .next/standalone/server.js`
3. **Set `output: 'standalone'`** for Docker
4. **Configure cache handler** for multi-instance ISR
5. **Set `HOSTNAME="0.0.0.0"`** for containers
6. **Copy `public/` and `.next/static/`** - not included in standalone
7. **Add health check endpoint**
8. **Test ISR revalidation** after deployment
9. **Monitor memory usage** - Node.js defaults may need tuning
## Testing Cache Handler
**Critical**: Test your cache handler on every Next.js upgrade:
```bash
# Start multiple instances
PORT=3001 node .next/standalone/server.js &
PORT=3002 node .next/standalone/server.js &
# Trigger ISR revalidation
curl http://localhost:3001/api/revalidate?path=/posts
# Verify both instances see the update
curl http://localhost:3001/posts
curl http://localhost:3002/posts
# Should return identical content
```
@@ -1,67 +0,0 @@
# Suspense Boundaries
Client hooks that cause CSR bailout without Suspense boundaries.
## useSearchParams
Always requires Suspense boundary in static routes. Without it, the entire page becomes client-side rendered.
```tsx
// Bad: Entire page becomes CSR
'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
return <div>Query: {searchParams.get('q')}</div>
}
```
```tsx
// Good: Wrap in Suspense
import { Suspense } from 'react'
import SearchBar from './search-bar'
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchBar />
</Suspense>
)
}
```
## usePathname
Requires Suspense boundary when route has dynamic parameters.
```tsx
// In dynamic route [slug]
// Bad: No Suspense
'use client'
import { usePathname } from 'next/navigation'
export function Breadcrumb() {
const pathname = usePathname()
return <nav>{pathname}</nav>
}
```
```tsx
// Good: Wrap in Suspense
<Suspense fallback={<BreadcrumbSkeleton />}>
<Breadcrumb />
</Suspense>
```
If you use `generateStaticParams`, Suspense is optional.
## Quick Reference
| Hook | Suspense Required |
|------|-------------------|
| `useSearchParams()` | Yes |
| `usePathname()` | Yes (dynamic routes) |
| `useParams()` | No |
| `useRouter()` | No |
@@ -0,0 +1,152 @@
---
name: next-cache-components-adoption
description: "启用 Next.js Cache Components 并修复阻塞路由。"
---
# next-cache-components-adoption
Enable Cache Components on an app and walk it to a clean build. This skill **sequences** the work; per-error recipes live in the dev overlay fix cards, stack traces, and `/docs/messages/blocking-prerender-*` pages.
## requires
Confirm each item before starting milestone A. The skill won't apply cleanly if any are unmet.
- **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs.
- **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first:
- `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
- **No incompatible config keys.** `cacheComponents: true` errors on any page that still exports `dynamic`. Other legacy route segment configs (`revalidate`, `fetchCache`) and the renamed `experimental.dynamicIO` / `experimental.useCache` should also be migrated. Resolve them via the [migration guide's "Enable Cache Components" section](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components) before starting.
### notes
- **No green baseline before the flag.** If the app already uses `"use cache"`, the pre-flag build errors with `please enable the feature flag cacheComponents`. Enabling the flag is the first step of milestone A, not a thing to do _after_ getting green; the green baseline comes from milestone A (blanket the opt-outs in), not from before it. Note this in your starting summary so it doesn't read as a regression.
- **Offline docs.** Offline copies of guide links live under `node_modules/next/dist/docs/`, with the directory layout numbered for ordering (e.g. `node_modules/next/dist/docs/01-app/02-guides/migrating-to-cache-components.md`). The trailing filename matches the slug. If you can't predict the numbered prefix, `find node_modules/next/dist/docs -name '<slug>.md'` resolves it. The `/docs/messages/*` error pages are not bundled. If offline docs are missing entirely, run `npx @next/codemod@latest agents-md` to write a version-matched index into `AGENTS.md` / `CLAUDE.md`.
## the shape of the work
Adoption has two milestones. Each is shippable on its own:
- **A. Green build.** `next build` passes with `cacheComponents: true` — blanket `instant = false` if needed. Setup for B. (steps 12.)
- **B. Remove `instant = false`.** **This is the loop where adoption happens.** Walk the route tree top-down, one subtree at a time, removing each opt-out and either making the route prerenderable or documenting it as a deliberate Block — checking in with the user at each subtree boundary. Expect to spend most of the time here. (steps 23.)
**Adoption is complete after B.** Further optimization — making navigations instant, adopting Partial Prefetching, locking the result in with e2e tests, growing static shells — is covered by the linked guides in [further reading](#further-reading). Point the user at them; this skill doesn't walk through them.
**End of every milestone: summarize and ask.** Tell the user which routes changed and how (cached / wrapped in `<Suspense>` / opted out as a documented Block), what they should sanity-check, and ask whether to open a PR before continuing. Each milestone is a real checkpoint, not a step inside one agent run. Don't silently roll on.
## background
`cacheComponents: true` requires every route to be prerenderable. A route that reads request-time data outside `<Suspense>` is "blocking" and **fails the build**. `export const instant = false` marks a route as allowed to block, which clears it in both dev and build; on a layout it covers the whole subtree beneath it.
**`instant = false` does not clear sync-IO errors.** Unstable values evaluated at module/render time — `new Date()`, `Date.now()`, `Math.random()`, `crypto.randomUUID()` — still fail the prerender (`blocking-prerender-current-time` / `-random` / `-crypto`) even with the opt-out, because they produce a different result on every render and can't be baked into a static shell. So the blanket codemod gets the build green **only if no shared layout or page calls one of these directly**; if one does, you must fix it regardless of `instant = false`. Follow the fix cards on the error page itself — they own the per-API recipe. This most often bites in a shared layout, where one `new Date()` blocks every route under it.
## surfacing errors
Two surfaces; they show different things.
**`next build` — detection only.** Use it to confirm milestone A (green build) and to spot-check milestone B (no route opted out). It stops at the first blocking route, so it's poor for sizing the work. Two flags help when iterating: `--debug-build-paths` builds only the routes you name (comma-separated glob patterns of **file paths relative to the project root**, e.g. `--debug-build-paths="app/admin/**/page.tsx"` or `--debug-build-paths="app/(marketing)/about/page.tsx"` — not URL paths; `--debug-build-paths=/admin` matches nothing and silently exits 0) and `--debug-prerender` (dev-only) prints a fuller stack trace so the error names the originating file and line.
**`next dev` — the working surface.** Visit a route; its blocking errors surface in the dev overlay with full stack traces and fix cards linking the per-error docs. Work one route at a time — errors don't accumulate in one place. The route itself still returns HTTP 200, so don't gate on status codes; read the overlay (or `.next-dev.log` if you can't drive a browser yet).
**Verifying a fix at runtime.** A green build or a cleared overlay isn't proof the route actually behaves — Cache Components is a runtime concern (a static shell with streamed data). Load the route in a real browser, wait for streaming to settle, and confirm it renders. Three ways, in order of preference:
1. **The [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) skill** is the fastest path: it cross-checks `/_next/mcp` against the live browser. Install if your agent doesn't have it:
```bash
npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop
```
It has its own hard prerequisites (Turbopack and `agent-browser >= 0.27.0`) and will tell you how to set those up.
2. **A browser you can drive yourself** (Playwright, agent-browser, any browser-automation tool). `next-dev-loop` is an accelerator, not a prerequisite.
3. **No browser at all?** Ask the user. Either ask them to drive the dev server and report what the overlay shows, or commit the milestone you've reached and hand off. **Don't silently stop at A or B and call it done** — the runtime behavior won't surface in the build. Be explicit about what you couldn't verify.
Verify after every fix, not only at the end. Don't fall back to grepping source or trusting the build alone.
## step 1: choose a strategy
Ask the user; don't assume. **In a non-interactive run** (no way to prompt), default to **Blanket** for a multi-route app and **Direct** for a single-route or handful-of-routes app, and say so when you start.
- **Blanket** — run the codemod to opt every page and layout out, get a clean build immediately, **merge that**, then remove the opt-outs feature by feature in follow-up PRs. Use for large apps, team repos (a long-lived failing branch blocks others), or when you can't land every route in one PR.
- **Direct** — enable the flag and fix every route in place in one pass. Use for small or solo apps where one PR is realistic.
### blanket
```bash
npx @next/codemod@canary cache-components-instant-false ./app
```
If `@next/codemod@latest` reports `Invalid transform choice`, try `@canary` — new transforms land there first.
Inserts `export const instant = false` (with a `// TODO: Cache Components adoption` comment) into every `app/**/{page,layout,default}` file, skipping files that already declare `instant` and any module marked `"use client"` or `"use server"`. Then set `cacheComponents: true`. The TODO comments are the work queue for milestone B.
**If the codemod isn't available** (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand: for every `app/**/{page,layout,default}.{js,jsx,ts,tsx}` that isn't `"use client"` or `"use server"` and doesn't already declare or export `instant` in any form, insert the three-line block below after the file's import statements (or at the top, if there are none):
```ts
// TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.
// See: https://nextjs.org/docs/app/guides/migrating-to-cache-components
export const instant = false
```
Then set `cacheComponents: true`. The result is the same as what the codemod produces.
The codemod opts **every** segment out, not only the root, on purpose. Resolution is top-down, first-explicit-config-wins: the **highest** `instant = false` in a route's tree decides the whole subtree, and deeper ones are never read. If you only opted the root layout out, removing it would re-arm validation for the entire app at once. With an opt-out on every segment, removing one segment's opt-out validates only **that** segment — its descendants keep their own opt-outs and stay green, so the blast radius is one segment at a time.
Because the highest opt-out wins, **remove them top-down** (root first, then descend). Removing a leaf's opt-out does nothing while an ancestor still holds one.
**Confirm milestone A with a build.** Run `next build` and make sure it completes with no blocking-route errors before you call the green build done. The codemod gets you most of the way, but a shared layout that calls `new Date()` / `Math.random()` directly still fails regardless of the opt-out (see "background" above), so the build is the proof, not the codemod run.
After running the codemod, **confirm the root layout got an opt-out** (`grep -n "export const instant" app/layout.*`). The root layout is the one segment that must be covered: it renders every route, including framework routes like `/_not-found`, so if it still reads `cookies()` without an opt-out the build fails on `/_not-found` even though no other route changed. If it was missed, add `export const instant = false` to it by hand.
**Never add `instant = false` to a synthetic route** like `/_not-found` — there is no user file for it, and the directive wouldn't apply. When `/_not-found` (or another framework route) blocks, the cause is the **root layout** it renders through; fix the opt-out there.
**Client Components (`"use client"` pages/layouts) get no opt-out** — the codemod skips them on purpose. `instant` is a Server Component route segment config; exporting it from a client module is a build error (`E1344`). They don't need one anyway: a client page is covered by its nearest server layout's opt-out, and a client page can't read server request data (`cookies()`, `headers()`, `await params`) itself, so it rarely blocks on its own. If a route with a client page still blocks, the cause is server-side data in an ancestor layout — fix the opt-out or the read there, not on the client page.
### direct
Set `cacheComponents: true` and collect the errors. The reported routes are the work queue; there are no opt-outs to remove.
## step 2: remove opt-outs, one subtree at a time
The route tree is the work queue. Pick one subtree (`app/dashboard/**`, or a top-level app if the repo has several — marketing, app, docs), finish it end-to-end, ship it, then start the next. Each subtree is an independent, mergeable change. Don't fan out across the whole app in one pass — the point of milestone A's blanket was to make the loop incremental, not optional.
Within a subtree, walk **top-down** (layouts before the pages beneath them, root layout first). The root layout is often the hardest (it wraps `<html>` / `<body>` and frequently reads `cookies()`), but it shadows every route including framework routes like `/_not-found`, so it has to come off before anything below it can be validated. (Direct path: there are no opt-outs to remove — fix each failing route; if a hand-written opt-out on an ancestor shadows it, remove that first.)
**A green build mid-walk doesn't mean the layout is clean.** Removing a layout's opt-out while its descendant pages still have theirs keeps the build green — each page shadows the inherited validation. The layout's actual blocking reads only surface once nothing below it shadows them. So after a layout is opt-out-free, **keep going** down the subtree; if the layout has an inherent blocker, the first page you uncover will be the one to surface it. Don't call a subtree done at the layout boundary.
For each route in the subtree:
1. Remove its `instant = false` (Blanket) or target the failing route (Direct).
2. Reload it in dev or rebuild only that route. If it's clean, the route was already prerenderable — move on.
3. If it still blocks, read the error in the dev overlay and apply the fix it points at. When the call gets ambiguous — you're not sure which fix fits, the blocking code looks security-sensitive, or the user might want to keep the route blocking on purpose — read **[references/per-page-decisions.md](./references/per-page-decisions.md)** before editing. Those cases are user check-in moments, not agent judgment calls.
4. Re-check the route. If your fix touched shared code (a layout, a sidebar component), re-check sibling routes too — a shared-shell change can fix the route you're on and break a sibling. Then move to the next route.
Keep a todo list of the subtree's routes and work it to completion; don't truncate. When every route in the subtree is clean, move to **step 3** to verify and hand the subtree off to the user.
## step 3: verify the subtree
A checklist, not new adoption work. This is where the user signs off on the subtree before you start the next.
- `next build` completes without blocking-route errors.
- No bare `// TODO: Cache Components adoption` opt-outs are left in the subtree (`grep` to confirm). Any `instant = false` left behind must be a **deliberate, documented Block** — its comment rewritten to a reason (see [references/per-page-decisions.md](./references/per-page-decisions.md) → "when to leave a Block in place"), not the original `// TODO`.
- Drive each route in dev, not only the build. Visit it, wait for streaming to settle, confirm every `<Suspense>` fallback resolves to its real content (not stuck on a skeleton or a blank). A green build with zero opt-outs is not the same as a working route. Query the live DOM if a tool's snapshot looks stale before reporting a route as broken.
- **Show the user the rendered result.** A screenshot or the visible content you observed, per route. The build can't tell whether the streamed-in loading state, the fallback, or the final layout matches what the user wants. Adoption changes the _experience_, so the person who owns the product should sign off on each piece.
**Expect some routes to still print `ƒ` (Dynamic) in the build's route table — that is success, not a regression.** A route comes out `ƒ` when it does request-time work through the documented escape hatch (e.g. a layout that `await connection()` for `new Date()`); the page is no longer _opted out_, it is genuinely dynamic. Don't rip the escape hatch back out chasing a ``. The inverse also holds: `instant = false` does **not** force a route to be `ƒ`. The glyph reflects what the route does at prerender time, not which validation knobs it exports.
When the subtree passes and the user is happy with each route, **summarize and ask**: open a PR and move to the next subtree, or stop here?
Milestone B is done only when **every** subtree is clean — every remaining `instant = false` sits under a reason comment, no bare TODOs are left (`grep -rln "TODO: Cache Components adoption" app` returns nothing). **Adoption is complete here.** Point the user at [further reading](#further-reading) if they want to push the experience further, or stop and ship.
## further reading
Adoption ends at milestone B. The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.
- **[Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation)** — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a green build doesn't guarantee that — a `<Suspense>` above the shared layout caught the page-load case but doesn't cover client navigation).
- **[Adopting Partial Prefetching](https://nextjs.org/docs/app/guides/adopting-partial-prefetching)** — walks an audit of `<Link prefetch={true}>` calls driven by the dev overlay's `link-prefetch-partial` warning, then flips the `partialPrefetching` config. **Walk the audit first, with the flag off** — flipping it before the audit makes every route count as adopted, so the warnings never fire and the per-link signal is lost. The biggest payoff of Cache Components: `<Link>` prefetches only the static App Shell by default. Recommended after instant navigation, since its fixes feed directly into how much of each route the shell can prefetch.
- **[Prefetching](https://nextjs.org/docs/app/guides/prefetching)** and **[Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching)** — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap.
- **[Locking the result in with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests)** — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- **[`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer)** — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.
@@ -0,0 +1,25 @@
# per-page decisions: removing `instant = false`
Read this when a route still blocks after you remove its `instant = false` and the dev overlay's fix card isn't enough on its own. Each section here covers a judgment call the agent shouldn't make alone.
## deciding what to do with a blocking read
Read the full linked page behind the fix card — not only the inline snippet — before editing. The card unblocks the build, but the page covers the details that make the route's navigation actually instant (e.g. where to place a `<Suspense>` boundary). Don't improvise.
If you're unsure which fix fits, the right call usually depends on what this part of the page is _for_, which the code doesn't capture. Ask the user about their goal for it rather than guessing. Frame it as a product/UX question: should this content be there instantly on load, or is it fine for it to stream in a moment later? Should everyone see the same thing (cacheable) or is it per-user / per-request? Tie the technical fix to that answer (cache it, wrap it in `<Suspense>`, or keep it request-time), so they're deciding the experience, not the API.
## security gates and other code you can't infer
If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `<Suspense>` would change what the code guarantees — stop and ask the user before refactoring. The build error wants `<Suspense>`, but wrapping a gate in `<Suspense>` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, or move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy).
If _every_ route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on.
For the broader picture, read the [Authentication guide](https://nextjs.org/docs/app/guides/authentication) (where auth checks belong: Proxy for routing, Data Access Layer for data) and the [Data Access Layer section of Data Security](https://nextjs.org/docs/app/guides/data-security#data-access-layer) (centralized auth checks that compose with `'use cache'`).
If you don't know how to make a piece of code Cache Componentscorrect without changing what it does, ask.
## when to leave a Block in place
If a route is genuinely meant to block — it's inherently per-request with no useful static shell — or the refactor would be large and the user would rather not take it on now, that's a legitimate outcome. Keep `instant = false`, but confirm it with the user first and turn its `// TODO: Cache Components adoption` comment into a reason, e.g. `// instant = false: kept on purpose — fully request-time dashboard` or `// instant = false: deferred, refactor too large for now`.
A documented, deliberate Block is fine to leave after the migration; an undocumented leftover opt-out is not.
@@ -0,0 +1,128 @@
---
name: next-cache-components-optimizer
description: "优化 Next.js Cache Components 页面静态壳和导航体验。"
---
# next-cache-components-optimizer
Two loops, shared levers and primitives, different diagnostics:
- **Page-render loop** ([ppr-loop.md](./ppr-loop.md)) — grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render.
- **Nav loop** ([instant-nav-loop.md](./instant-nav-loop.md)) — when the user clicks a link from A to B, show B's static layout immediately (chrome, structure, content-shaped fallbacks) instead of holding A's UI until B's data resolves. Capture B's suspended boundaries post-`pushstate`, classify each by `suspended_by[].name`, drop SSR-only client hooks.
Pick one and run it end-to-end.
## requires
- `next-dev-loop` initiated for this session — it opens the headed browser, exposes the `agent-browser` CLI, and wires the dev MCP server that provides `mcp get_logs`.
- `cacheComponents: true` in `next.config.ts`. Refuse otherwise.
## preflight (shared)
1. Confirm `cacheComponents: true`.
2. **The user must already be at the page each loop needs** in the headed browser (from `next-dev-loop`) — logged in, with any state set up. This skill can't drive auth, SSO, or MFA; it takes the manual setup as the starting point. (Each sub-loop names which page it expects.)
3. `agent-browser get url` to anchor the current route.
Each loop sets the instant cookie as needed (see the shared `instant cookie` section below).
## instant cookie (shared)
Both loops use the `next-instant-navigation-testing` cookie to freeze the framework's dynamic-data writes. Once set, visible content on the page is the static shell + Suspense fallbacks — that's what we capture to assess the optimization.
Set it with a pending-lock tuple `[0, "<unique-id>"]`. The id is any unique string; the convention is a `p`-prefixed random stamp so concurrent scopes don't collide:
```
agent-browser cookies set next-instant-navigation-testing '[0,"p<random>"]' \
--url <origin>
```
Each loop's preflight specifies when to set it within the flow. Clear it at the end (see `teardown` below).
## decide which loop
- **Page-render** when the complaint is about one route's initial load. Read [ppr-loop.md](./ppr-loop.md).
- **Nav** when it's about navigating between two routes. Read [instant-nav-loop.md](./instant-nav-loop.md).
Ambiguous → ask.
## shared refactor levers
- **Push down** — extract I/O into a Suspense-wrapped child so the parent stays static and static siblings lift into the shell.
- **Recurse, don't blind-wrap.** If a Suspense boundary already wraps a component containing both static content and the I/O, read inside, extract the I/O-dependent JSX into a new leaf, and lift the static siblings up.
- **Cache** — `'use cache'` + `cacheLife(<profile>)`. Always ask the user for freshness; map to a preset (`seconds` / `minutes` / `hours` / `days` / `weeks` / `max` / `default`).
Push-down and cache compose: push-down lifts static structure, cache eliminates the remaining data gap.
## propose via plan mode (shared)
Each refactor goes through plan mode before applying. Treat this as a signal: the application work is non-trivial agentic engineering, not a templated edit. This skill provides the framework — which lever to reach for, which candidate to fix, what the expected visible delta is — but the real work (which file to edit, how to cleanly extract the I/O, where to place the new Suspense boundary, which `cacheLife` profile to ask the user for) is a judgment call you have to think through. Plan mode forces a coherent proposal before touching code, and gives the user a chance to redirect on any of those decisions.
## no-shell bailout (shared)
The levers presume a shell exists to grow or cache toward. If the route is fully blocking — HTTP 500 with `blocking-route` or `NEXT_STATIC_GEN_BAILOUT` in `mcp get_logs`, or zero Suspense boundaries on a visibly-rendered page — there's no shell. Surface the structural blocker and stop; the user has to wrap the offending dynamic access in `<Suspense>` before either loop can help.
## verify requires a visible delta (shared)
Each loop captures a baseline screenshot of the shell before applying any change, then re-screenshots after. Report both paths in the final summary so the user can see what changed. The two captures must visibly differ — fallback area shrunk, content promoted to the static surface, target fallback gone or content-shaped. Identical-looking captures mean the refactor didn't land; undo. "Compiles cleanly" is not the bar.
**Hide the dev overlay before each screenshot.** The Next.js dev overlay (`<nextjs-portal>` at the document root) renders instant-nav guidance, build errors, and other dev chrome that pollute the before/after comparison. Hide it, screenshot, restore:
```
agent-browser eval "document.querySelector('nextjs-portal').style.display='none'"
agent-browser screenshot <path>
agent-browser eval "document.querySelector('nextjs-portal').style.display=''"
```
## anti-patterns (shared)
**Don't replace granular Suspense boundaries with a top-level loading skeleton.** A `loading.tsx` for the whole segment, or a root-level `<Suspense fallback={<Skeleton />}>` (or worse, `fallback={null}` that blanks the UI), defeats this skill's optimization — which is to extract real static chrome above each granular boundary and use content-shaped fallbacks per region. A coarse "the page is loading" stand-in bypasses the work entirely.
## gotchas (shared)
- Dev doesn't prefetch the way production does, and routes compile on first hit — so after a navigation or reload, the DOM keeps updating for noticeably longer than the eventual production experience. Wait patiently for the DOM to stabilize before capturing the React tree or taking a screenshot — e.g., poll `document.documentElement.innerHTML.length` until it's unchanged across two consecutive reads. A fixed short delay risks sampling mid-render.
- Don't try to verify nav prefetch by inspecting dev network traffic — dev doesn't fire prefetch requests at all, so the network tab, manual `router.prefetch()` calls, and `<Link prefetch={true}>` will all look broken regardless of whether your code is correct. The cookie-locked SPA-nav recipe in [instant-nav-loop.md](./instant-nav-loop.md) under `verify` is already the canonical recipe for this — it simulates what production would prerender into the prefetched RSC without requiring prefetch to actually fire. Use it; don't invent a network-tab alternative.
- The diagnose pipeline can be flaky — DevTools attachment timing, DOM-settle races, and dev compilation effects can each produce inconsistent captures from one run to the next. When a result feels off (a candidate appears that you don't expect, or one you expect doesn't), re-run the diagnose 23 times and cross-check; boundaries that appear consistently are real, one-off appearances are noise.
## reference (shared primitives)
```
agent-browser react suspense add --only-dynamic to filter
--json server-side to actually-
suspended boundaries. Each
entry has jsx_source +
suspended_by[] with raw blocker
names (usePathname, cookies,
fetch, cache, ...); classify by
name for per-loop rules
POST /__nextjs_original-stack-frames body { frames: StackFrame[],
isServer, isEdgeServer,
isAppDirectory }; returns one
result per frame with
file:line:column
mcp get_logs dev MCP tool from
next-dev-loop; surfaces
blocking-route /
NEXT_STATIC_GEN_BAILOUT 500s
cacheLife('<profile>') default | seconds | minutes
| hours | days | weeks | max
```
Per-loop primitives in [instant-nav-loop.md](./instant-nav-loop.md).
## teardown (shared)
Delete the cookie by name — overwrite with an expired stamp:
```
agent-browser cookies set next-instant-navigation-testing x \
--url <origin> --expires 1
```
Never `agent-browser cookies clear` (no args) — wipes auth.
---
Sibling of `next-dev-loop` — initiate that first.
@@ -0,0 +1,89 @@
# instant-nav-loop (sub-reference of next-cache-components-optimizer)
In-app navigation optimization: when the user clicks a link from A to B, show B's static layout immediately — chrome, structure, content-shaped fallbacks — instead of holding A's UI until B's data resolves.
Strictly smaller than page-render — only segments newly mounted on B's path need a shell. The LCA layout stays mounted with its already-resolved data, so even a dynamic-root app can have instant in-app nav.
The hard part is **identifying real blockers**. After `pushstate`, capture B's suspended boundaries with `agent-browser react suspense --only-dynamic --json`. Each boundary carries `suspended_by[].name``usePathname` / `useSearchParams` / `useRouter` (client-hook), `cookies` / `headers` / `connection` (request-api), names containing `fetch` or `cache` (server-fetch / cache), etc.
**Client-hook blockers don't block instant nav.** They suspend only during SSR prerender; on a client nav (`pushstate`) they resolve instantly from the router store. Diagnose drops them as candidates (see step 4 for the operational set), so the loop doesn't recommend SSR-only refactors that have no effect on click-to-paint.
When the user navigates from A to B, the layouts they share stay mounted; only the segments past the point where the two routes diverge actually load. So B's suspended-boundary capture is naturally focused on the new work — you don't have to filter out the shared parts. With multiple real candidates, fix the one highest in the new-segments tree (closest to where A's and B's paths diverge) first.
Complementary to Next.js's Instant Insights, which checks Suspense _existence_ (structural). This loop checks fallback _quality_ (visual) — closing the `<Suspense fallback={null}>` loophole.
## preflight (in addition to shared)
The starting page is **A** — the route the user is navigating _from_. The shared preflight has already anchored A (the user is on it, logged in, with state set up). Now anchor B:
1. Ask the user to perform the navigation to B in the headed browser — click the link, button, or whatever leads there. Read `agent-browser get url` to confirm B's URL.
2. `agent-browser pushstate <A>` to return the browser to A. The diagnose loop starts from A.
Set the instant cookie (per shared `instant cookie` section) any time after the browser is on A but before calling `pushstate`. There's no race — the cookie only needs to be present at the moment of navigation. The cookie doesn't block the navigation; it gates the framework's dynamic-data writes, so B's React tree mounts normally but its dynamic Suspense boundaries stay in fallback until the cookie is cleared. (Setting the cookie before a direct load of A or B would freeze that page at its static shell — that's why we wait until the user is on A first.)
## loop
### diagnose
1. **Set the instant cookie and navigate via `pushstate <B>`.** Wait for the DOM to settle. B's tree is now mounted; its dynamic boundaries stay in fallback while the cookie holds.
2. **Check B for the no-shell bailout** per SKILL.md.
3. **Capture B's suspended set.** `agent-browser react suspense --only-dynamic --json` → boundaries with `suspended_by[]`.
4. **Filter to real nav candidates.** Drop boundaries whose `suspended_by` entries are **all** client-hook names (`usePathname`, `useSearchParams`, `useRouter`, `useSelectedLayoutSegment(s)`, `useParams`) — SSR-only, won't help nav. Keep boundaries with at least one request-api, server-fetch, or cache blocker.
5. **Gauge the gap.**
- No candidates → ask the user if the nav still feels slow. No → stop and offer to audit other A → B pairs. Yes → step 8 (unwrapped async).
- Candidates present → screenshot the locked B state (hide `nextjs-portal` per the shared rule), check rendered area. All sub-viewport → already in good shape; stop.
6. **Resolve sources** for each remaining candidate via `POST /__nextjs_original-stack-frames` on `suspended_by[].owner_stack` (or `jsx_source` if the stack is empty).
7. **Pick the highest candidate.** With multiple candidates, fix the one highest in B's new-segments tree — closest to where A's and B's paths diverge. If that candidate wraps the others, recurse: read inside the wrapper to find the I/O that's actually blocking.
8. **Fallback: unwrapped async.** Reached from step 5 when there are no `react suspense` candidates but the nav still feels slow. The blocker has no `<Suspense>` so it doesn't appear in the capture. Direct-load B (full page navigation, not pushstate; cookie still set); `mcp get_logs` surfaces a `blocking-route` 500 naming the unwrapped I/O. Filter to sources past the point where A's and B's paths diverge; bailouts in shared layouts above are PPR concerns, not nav.
### decide / apply
Apply the shared lever rules from SKILL.md; push-down recipes work at layouts too.
**Nav-only third lever: private cache + runtime prefetch.** For I/O that reads `cookies()` / `headers()` / `searchParams`, shared `'use cache'` won't help — those reads bail to dynamic. Use `'use cache: private'` + `cacheLife({ stale: N })` on **the scope that encloses the request-API read** (see scope rule below), plus `prefetch = 'allow-runtime'` as a route segment config (page or layout export) on the segment that owns the private content. Private-cache results live only in the browser — **never stored on the server** — so allowing runtime prefetching lets Next.js resolve them at link-visibility time with the user's session; the click commits with cookie-derived data already in place.
**Scope rule when cookie-read and data-fetch live in different frames.** The directive's semantics are about the cache scope enclosing the request-API call, not "the I/O function" as a label. If a page reads `cookies()` and passes the value into a separate fetch helper, putting `'use cache: private'` on the helper alone leaves the cookie read outside any cache scope and the segment stays dynamic. Either move the directive up to the frame that reads cookies, or move the cookie read down into the helper. Compiles and typechecks either way — only runtime behavior tells you which is correct.
The `prefetch` flag applies to the segment plus every descendant — put it on the most ancestral segment with runtime-cacheable content (layout-level covers all child pages; page-level covers only that page). Server Component only (not allowed with `"use client"`); requires `cacheComponents: true`. Valid values: `'auto'` (default) / `'force-disabled'` / `'force-static'` / `'allow-runtime'`. It doesn't make any segment cacheable on its own (each still needs `'use cache: private'`), doesn't help cold loads (no `<Link>` to prefetch from), and doesn't override `await connection()`.
### verify
**Cookie-locked SPA-nav screenshot** is the canonical visible delta — what production would have prefetched. Recipe (run once before applying, once after):
1. Be on A. Set the instant cookie.
2. `agent-browser pushstate <B>` — a real client navigation.
3. Wait for the DOM to settle. With the cookie set, the framework gates dynamic-data writes (see preflight), so the captured state is the static shell + Suspense fallbacks.
4. Hide `nextjs-portal`, screenshot, restore (per the shared rule).
Compare the two screenshots per the shared visible-delta rule. The after-shot must visibly differ — more static content promoted, content-shaped fallbacks, or (for the private + runtime-prefetch lever) cookie-derived data resolved.
**Identical before/after is two distinct signals, not one.** Either the lever didn't apply (code is wrong) or no I/O resolved either time (environment is broken — DB unreachable, stale TLS sockets, etc.). Before iterating on the code shape, check `mcp get_logs` for socket timeouts or fetch failures in either capture window. If the data path didn't complete in either run, the comparison is inconclusive — fix the environment and re-capture.
Also:
1. **Re-run the diagnose capture.** Repeat steps 14 on the new code. The target candidate should be absent from B's suspended set (the blocker is gone), or its fallback should now be content-shaped.
2. Re-check B for the no-shell bailout.
## reference (loop-specific)
```
agent-browser pushstate <url> client-side navigation (no
HTTP request, no full reload)
'use cache: private' per-session cache; cookies /
headers / searchParams reads ok
prefetch = 'allow-runtime' route-level: permits runtime
prefetching of private-cached
content when <Link> is visible
instant = false layout-level opt-out; escape
hatch, anti-pattern
```
@@ -0,0 +1,31 @@
# ppr-loop (sub-reference of next-cache-components-optimizer)
Page-render optimization: grow the static shell of a single `cacheComponents` page.
Rank candidates by visible pixel area; largest gap first.
## preflight (in addition to shared)
The page to optimize is whatever route the user is currently on (per shared preflight). Set the instant cookie (per shared `instant cookie` section), then reload.
## loop
### diagnose
1. **Check for the no-shell bailout** per SKILL.md.
2. **List candidates.** `agent-browser react suspense --only-dynamic --json` → each boundary has `jsx_source` (file:line:col) and `suspended_by[].name`. Resolve `jsx_source` (or `suspended_by[].owner_stack`) via `POST /__nextjs_original-stack-frames`.
3. **Rank by rendered area.** Per candidate, take max(fallback rect on shell-only, rendered subtree rect on full). Fallback rect alone misleads when developers used an undersized spinner.
4. **Gauge the gap.** Same capture as verify — the shell-only render. If the top-ranked candidate is sub-viewport (thin fallback bar, sidebar widget), the shell is already in good shape; surface that and offer to audit other routes for better targets, rather than forcing a marginal refactor.
5. **One boundary dominates →** that wrapper is the shell. Read inside, enumerate the awaits, recurse with those.
### decide / apply
Apply the shared lever rules from SKILL.md.
### verify
Re-take the shell-only render and compare against the baseline screenshot. The targeted gap must shrink or vanish; identical captures fail per the shared visible-delta rule. Re-check the no-shell bailout; a botched extract can break the shell.
@@ -1,411 +0,0 @@
---
name: next-cache-components
description: "Next.js 16 Cache Components、PPR、use cache、cacheLife、cacheTag 和 updateTag 工作流。"
---
# Cache Components (Next.js 16+)
Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
## Enable Cache Components
```ts
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
```
This replaces the old `experimental.ppr` flag.
---
## Three Content Types
With Cache Components enabled, content falls into three categories:
### 1. Static (Auto-Prerendered)
Synchronous code, imports, pure computations - prerendered at build time:
```tsx
export default function Page() {
return (
<header>
<h1>Our Blog</h1> {/* Static - instant */}
<nav>...</nav>
</header>
)
}
```
### 2. Cached (`use cache`)
Async data that doesn't need fresh fetches every request:
```tsx
async function BlogPosts() {
'use cache'
cacheLife('hours')
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
```
### 3. Dynamic (Suspense)
Runtime data that must be fresh - wrap in Suspense:
```tsx
import { Suspense } from 'react'
export default function Page() {
return (
<>
<BlogPosts /> {/* Cached */}
<Suspense fallback={<p>Loading...</p>}>
<UserPreferences /> {/* Dynamic - streams in */}
</Suspense>
</>
)
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value
return <p>Theme: {theme}</p>
}
```
---
## `use cache` Directive
### File Level
```tsx
'use cache'
export default async function Page() {
// Entire page is cached
const data = await fetchData()
return <div>{data}</div>
}
```
### Component Level
```tsx
export async function CachedComponent() {
'use cache'
const data = await fetchData()
return <div>{data}</div>
}
```
### Function Level
```tsx
export async function getData() {
'use cache'
return db.query('SELECT * FROM posts')
}
```
---
## Cache Profiles
### Built-in Profiles
```tsx
'use cache' // Default: 5m stale, 15m revalidate
```
```tsx
'use cache: remote' // Platform-provided cache (Redis, KV)
```
```tsx
'use cache: private' // For compliance, allows runtime APIs
```
### `cacheLife()` - Custom Lifetime
```tsx
import { cacheLife } from 'next/cache'
async function getData() {
'use cache'
cacheLife('hours') // Built-in profile
return fetch('/api/data')
}
```
Built-in profiles: `'default'`, `'minutes'`, `'hours'`, `'days'`, `'weeks'`, `'max'`
### Inline Configuration
```tsx
async function getData() {
'use cache'
cacheLife({
stale: 3600, // 1 hour - serve stale while revalidating
revalidate: 7200, // 2 hours - background revalidation interval
expire: 86400, // 1 day - hard expiration
})
return fetch('/api/data')
}
```
---
## Cache Invalidation
### `cacheTag()` - Tag Cached Content
```tsx
import { cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheTag('products')
return db.products.findMany()
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`)
return db.products.findUnique({ where: { id } })
}
```
### `updateTag()` - Immediate Invalidation
Use when you need the cache refreshed within the same request:
```tsx
'use server'
import { updateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data })
updateTag(`product-${id}`) // Immediate - same request sees fresh data
}
```
### `revalidateTag()` - Background Revalidation
Use for stale-while-revalidate behavior:
```tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: FormData) {
await db.posts.create({ data })
revalidateTag('posts') // Background - next request sees fresh data
}
```
---
## Runtime Data Constraint
**Cannot** access `cookies()`, `headers()`, or `searchParams` inside `use cache`.
### Solution: Pass as Arguments
```tsx
// Wrong - runtime API inside use cache
async function CachedProfile() {
'use cache'
const session = (await cookies()).get('session')?.value // Error!
return <div>{session}</div>
}
// Correct - extract outside, pass as argument
async function ProfilePage() {
const session = (await cookies()).get('session')?.value
return <CachedProfile sessionId={session} />
}
async function CachedProfile({ sessionId }: { sessionId: string }) {
'use cache'
// sessionId becomes part of cache key automatically
const data = await fetchUserData(sessionId)
return <div>{data.name}</div>
}
```
### Exception: `use cache: private`
For compliance requirements when you can't refactor:
```tsx
async function getData() {
'use cache: private'
const session = (await cookies()).get('session')?.value // Allowed
return fetchData(session)
}
```
---
## Cache Key Generation
Cache keys are automatic based on:
- **Build ID** - invalidates all caches on deploy
- **Function ID** - hash of function location
- **Serializable arguments** - props become part of key
- **Closure variables** - outer scope values included
```tsx
async function Component({ userId }: { userId: string }) {
const getData = async (filter: string) => {
'use cache'
// Cache key = userId (closure) + filter (argument)
return fetch(`/api/users/${userId}?filter=${filter}`)
}
return getData('active')
}
```
---
## Complete Example
```tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
export default function DashboardPage() {
return (
<>
{/* Static shell - instant from CDN */}
<header><h1>Dashboard</h1></header>
<nav>...</nav>
{/* Cached - fast, revalidates hourly */}
<Stats />
{/* Dynamic - streams in with fresh data */}
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</>
)
}
async function Stats() {
'use cache'
cacheLife('hours')
cacheTag('dashboard-stats')
const stats = await db.stats.aggregate()
return <StatsDisplay stats={stats} />
}
async function Notifications() {
const userId = (await cookies()).get('userId')?.value
const notifications = await db.notifications.findMany({
where: { userId, read: false }
})
return <NotificationList items={notifications} />
}
```
---
## Migration from Previous Versions
| Old Config | Replacement |
|-----------|-------------|
| `experimental.ppr` | `cacheComponents: true` |
| `dynamic = 'force-dynamic'` | Remove (default behavior) |
| `dynamic = 'force-static'` | `'use cache'` + `cacheLife('max')` |
| `revalidate = N` | `cacheLife({ revalidate: N })` |
| `unstable_cache()` | `'use cache'` directive |
### Migrating `unstable_cache` to `use cache`
`unstable_cache` has been replaced by the `use cache` directive in Next.js 16. When `cacheComponents` is enabled, convert `unstable_cache` calls to `use cache` functions:
**Before (`unstable_cache`):**
```tsx
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id) => getUser(id),
['my-app-user'],
{
tags: ['users'],
revalidate: 60,
}
)
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
```
**After (`use cache`):**
```tsx
import { cacheLife, cacheTag } from 'next/cache'
async function getCachedUser(id: string) {
'use cache'
cacheTag('users')
cacheLife({ revalidate: 60 })
return getUser(id)
}
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const user = await getCachedUser(id)
return <div>{user.name}</div>
}
```
Key differences:
- **No manual cache keys** - `use cache` generates keys automatically from function arguments and closures. The `keyParts` array from `unstable_cache` is no longer needed.
- **Tags** - Replace `options.tags` with `cacheTag()` calls inside the function.
- **Revalidation** - Replace `options.revalidate` with `cacheLife({ revalidate: N })` or a built-in profile like `cacheLife('minutes')`.
- **Dynamic data** - `unstable_cache` did not support `cookies()` or `headers()` inside the callback. The same restriction applies to `use cache`, but you can use `'use cache: private'` if needed.
---
## Limitations
- **Edge runtime not supported** - requires Node.js
- **Static export not supported** - needs server
- **Non-deterministic values** (`Math.random()`, `Date.now()`) execute once at build time inside `use cache`
For request-time randomness outside cache:
```tsx
import { connection } from 'next/server'
async function DynamicContent() {
await connection() // Defer to request time
const id = crypto.randomUUID() // Different per request
return <div>{id}</div>
}
```
Sources:
- [Cache Components Guide](https://nextjs.org/docs/app/getting-started/cache-components)
- [use cache Directive](https://nextjs.org/docs/app/api-reference/directives/use-cache)
- [unstable_cache (legacy)](https://nextjs.org/docs/app/api-reference/functions/unstable_cache)
@@ -0,0 +1,144 @@
---
name: next-dev-loop
description: "通过 Next.js dev MCP 与真实浏览器验证运行时行为。"
---
# next-dev-loop
The edit/verify rhythm during `next dev` — make a change, then
confirm it actually works at runtime, not only that the types or
the build are happy.
You verify through two views of the same running app:
- **`/_next/mcp`** — an HTTP endpoint Next.js exposes about itself.
Knows framework-specific things: routes, segments, RSC, server
actions, server logs, and errors as Next.js saw them. Call
`tools/list` for the current surface.
- **`agent-browser`** — a CLI that drives a real Chrome. Knows
framework-agnostic browser things: DOM, console, network, React
fiber, vitals. Run `agent-browser --help` for the current surface.
The two views cross-check each other.
## requires
- Next.js **16.3+** with **Turbopack**`/_next/mcp` plus the
proactive compile check via `get_compilation_issues`.
- `agent-browser` **>= 0.27.0** — when React introspection landed.
These are hard floors, not soft preferences. If anything is missing,
tell the user how to upgrade and stop. Don't fall back to grepping
source or to a weaker probe — this skill assumes both views are live
at the versions above.
- Upgrade Next.js: `pnpm next upgrade` (or `npx next upgrade`).
Docs: https://nextjs.org/docs/app/getting-started/upgrading
(version-16 guide:
https://nextjs.org/docs/app/guides/upgrading/version-16)
- Install or upgrade `agent-browser`: `npm i -g agent-browser@latest`.
If the CLI isn't on `PATH`, install it before continuing — preflight
expects to invoke it directly.
## preflight
Once per session, confirm both views are live.
1. **Open `agent-browser` at the target URL, restoring saved
login state when present.** Build the `open` command from:
- `--session <name>` where `<name>` is the project
directory basename.
- `--state ~/.agent-browser/sessions/<name>-default.json` if
that file exists. Omit on first run — a missing path fails
the open.
- `--headed --enable react-devtools`. **If the `agent-browser`
daemon is already running from a previous session**, launch
flags like `--headed` are ignored and a warning is printed
(`⚠ --headed ignored: daemon already running`). To change
them, run `agent-browser close` first, then re-open.
The browser is the user's. If state was not restored (first
run, expired session) and the page is gated, the user drives
the login — pause until they confirm. Session state is sticky:
you can't add `--enable react-devtools` after the session is
open, and `cookies set` on a not-yet-opened session creates a
sessionless cookie that silently fails to apply.
2. POST `tools/list` to `/_next/mcp`. Send
`Accept: application/json, text/event-stream`; responses are
SSE-framed, strip the `data: ` prefix before parsing JSON.
- Unreachable → either `next dev` isn't running, or Next.js is
below 16.3. Check `package.json` to disambiguate, then refuse.
- `get_compilation_issues` not in the list → Next.js below 16.3.
Refuse and tell the user to upgrade.
3. `mcp get_compilation_issues` doubles as a Turbopack probe.
An error response of `"Turbopack project is not available..."`
means the user is on webpack. Refuse — Turbopack is required.
4. `mcp get_routes` → your route map for the rest of the session.
## loop
### before the edit — narrow the scope
Ask the running app, not the codebase. `/_next/mcp` knows which
files rendered the current route; use those as your search scope.
Runtime introspection stays cheap as the codebase grows; agentic
search doesn't.
### after the edit — verify
Four failure modes. Check each:
- **Compiles** — `mcp get_compilation_issues`.
- **Runs without errors** — `/_next/mcp` (server and bubbled-up
browser errors both surface here).
- **Behaves as intended** — `agent-browser` drives the page; assert
what the user actually sees.
- **React-level behavior** — `agent-browser` with react-devtools
enabled exposes the component tree, props, state, and render
counts. Anchor framework-level checks here (extra renders,
server/client boundary shifts, suspense fallbacks) — DOM asserts
alone miss them.
Pick the specific tool from `tools/list` or `agent-browser
--help` rather than from memory.
## gotchas
- React introspection output is stale after navigation. Re-run.
- Non-3000 dev server: read the `next dev` banner; set
`NEXT_MCP_URL=http://localhost:<port>/_next/mcp`.
- `get_errors` and `get_page_metadata` need at least one navigation
to populate.
## reference
All tools below are present once preflight passes. If `tools/list`
is missing any of them, preflight should have refused — re-check.
```
# /_next/mcp notes
get_project_metadata projectPath, devServerUrl, bundler
get_routes fs-scan; no browser session needed
get_errors runtime + build; needs a browser session;
includes browser-side errors caught by the
dev server
get_page_metadata segment trie + routerType; needs a browser
session; use as a discovery shortcut for
which files power a route
get_logs returns logFilePath
get_server_action_by_id hashed id → file + functionName
get_compilation_issues Turbopack only; errors on webpack
("Turbopack project is not available")
```
## teardown
Close the `agent-browser` session — `--session` writes state
to disk so the next loop's `--state` restores login. Leave
`next dev` up for the next loop.
---
`next-dev-loop-<topic>` siblings (e.g. `next-dev-loop-rsc`, `next-dev-loop-debug`)
assume this preflight already ran; they pick up at the loop.
@@ -1,50 +0,0 @@
---
name: next-upgrade
description: "按照官方迁移指南和 codemods 升级 Next.js 项目。"
argument-hint: "[target-version]"
---
# Upgrade Next.js
Upgrade the current project to the latest Next.js version following official migration guides.
## Instructions
1. **Detect current version**: Read `package.json` to identify the current Next.js version and related dependencies (React, React DOM, etc.)
2. **Fetch the latest upgrade guide**: Use WebFetch to get the official upgrade documentation:
- Codemods: https://nextjs.org/docs/app/guides/upgrading/codemods
- Version-specific guides (adjust version as needed):
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/guides/upgrading/version-15
- https://nextjs.org/docs/app/guides/upgrading/version-14
3. **Determine upgrade path**: Based on current version, identify which migration steps apply. For major version jumps, upgrade incrementally (e.g., 13 → 14 → 15).
4. **Run codemods first**: Next.js provides codemods to automate breaking changes:
```bash
npx @next/codemod@latest <transform> <path>
```
Common transforms:
- `next-async-request-api` - Updates async Request APIs (v15)
- `next-request-geo-ip` - Migrates geo/ip properties (v15)
- `next-dynamic-access-named-export` - Transforms dynamic imports (v15)
5. **Update dependencies**: Upgrade Next.js and peer dependencies together:
```bash
npm install next@latest react@latest react-dom@latest
```
6. **Review breaking changes**: Check the upgrade guide for manual changes needed:
- API changes (e.g., async params in v15)
- Configuration changes in `next.config.js`
- Deprecated features being removed
7. **Update TypeScript types** (if applicable):
```bash
npm install @types/react@latest @types/react-dom@latest
```
8. **Test the upgrade**:
- Run `npm run build` to check for build errors
- Run `npm run dev` and test key functionality
@@ -5,5 +5,5 @@
"commit": "54ef7c733480ca44061fc76d6d29dcf2227ecd67",
"adapter": "claude-skill",
"sourcePath": "skills/ppt-master",
"syncedAt": "2026-06-23T16:00:00Z"
"syncedAt": "2026-06-24T03:56:08Z"
}
@@ -5,5 +5,5 @@
"commit": "b59f68ecc5216e39d3f492e79b9767388ccd43db",
"adapter": "claude-skill",
"sourcePath": "skills/shadcn",
"syncedAt": "2026-06-23T16:00:00Z"
"syncedAt": "2026-06-24T03:56:08Z"
}
@@ -1,6 +1,6 @@
{
"name": "ui-ux-pro-max",
"version": "2.5.0",
"version": "2.6.2",
"description": "面向界面设计、视觉风格、配色、字体、可访问性和前端技术栈的 UI/UX 设计智能库。",
"author": {
"name": "NextLevelBuilder"
@@ -2,8 +2,8 @@
"sourceId": "ui-ux-pro-max",
"repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
"ref": "main",
"commit": "1518fec29d19ce905cd0c689255137b9dcab7ccc",
"commit": "a13b2a02fd58d8ad325fefd9d146f62708be8456",
"adapter": "claude-skill",
"sourcePath": ".claude/skills/ui-ux-pro-max",
"syncedAt": "2026-06-23T16:00:00Z"
"syncedAt": "2026-06-24T03:56:08Z"
}