skills/_templates/tech/nextjs-15/SKILL.md
Next.js 15 App Router patterns: Server Components, Server Actions, data fetching, middleware. Trigger: When building Next.js apps, working with app router, server/client components, or API routes.
npx skillsauth add fearovex/claude-config nextjs-15Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Triggers: When building Next.js apps, working with app router, server/client components, or API routes.
Load when: building Next.js 15 apps, using app router, implementing server actions, fetching data, or setting up middleware.
// ✅ Server Component — async by default, no directive needed
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findById(userId); // Direct DB access
return <ProfileCard user={user} />;
}
// ✅ Client Component — only when you need interactivity
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
await db.users.create({ name, email });
revalidatePath('/users');
redirect('/users');
}
// Direct usage in form
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Create</button>
</form>
);
}
// lib/db.ts
import 'server-only'; // Build error if imported in client
export async function getSecretData() {
return db.secrets.findAll();
}
// ✅ Parallel fetching in Server Component
async function Dashboard() {
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats(),
]);
return <DashboardView user={user} posts={posts} stats={stats} />;
}
// ✅ Streaming with Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Header /> {/* Immediate */}
<Suspense fallback={<PostsSkeleton />}>
<Posts /> {/* Streams when ready */}
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Waits here
return <PostList posts={posts} />;
}
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') ?? '1';
const users = await db.users.findMany({ page: parseInt(page) });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
// Static
export const metadata = {
title: 'My App',
description: 'App description',
};
// Dynamic
export async function generateMetadata({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}
app/
├── (auth)/ # Group with no URL impact
│ ├── layout.tsx # Layout only for auth pages
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── overview/page.tsx # /overview
├── _components/ # Private folder (not a route)
├── layout.tsx # Root layout (required)
└── page.tsx # /
// ❌ Unnecessary
'use client';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ✅ Direct Server Component
async function UserList() {
const users = await db.users.findMany();
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// ❌ Makes the entire tree client-side
'use client';
export default function Layout({ children }) { /* ... */ }
// ✅ Isolate the client component
export default function Layout({ children }) {
return <div><NavBar />{children}</div>; // NavBar can be 'use client'
}
| Task | Pattern |
|------|---------|
| DB in component | Server Component + async/await |
| Form | <form action={serverAction}> |
| Invalidate cache | revalidatePath('/path') |
| Redirect | redirect('/path') (server-only import) |
| URL params | { params }: { params: { id: string } } |
| Search params | searchParams.get('key') in Server Component |
| Protect routes | middleware.ts at root |
| Prevent client bundle | import 'server-only' |
'use client' only when the component requires browser APIs, event handlers, or React state'use client' to layout or page files — this forces the entire subtree client-side and defeats Server Component benefits'use server') must be the mechanism for mutations from forms; avoid client-side fetch for form submissionsrevalidatePath or revalidateTag must be called after mutations that change cached data; stale caches are a correctness bugimport 'server-only' must be added to any module that accesses secrets, databases, or server-only APIs to prevent accidental client bundlingdevelopment
Governs AI-assisted generation of images, video, and audio (Gemini Nano Banana Pro, FLUX, etc.) from any project, with a focus on key security and cost control. Trigger: generate an image/illustration/asset with AI, "generate an image", nano banana, gemini image, generate video, configure an image API.
business
Turns an already-investigated customer issue into a short, non-technical engineering-to-CS brief: one natural message that leads with the finding (root cause, real scope, open question), ready to paste into Slack for the support team. Trigger: /support-brief, support brief, brief for support, resumen soporte.
development
Parks the current Claude Code session before going to sleep. Analyzes the conversation, writes a handoff document to docs/handoffs/ in the current project, mirrors the same summary to engram tagged with the session ID, and prints the exact `claude --resume <id>` command for tomorrow. Zero interaction — runs end-to-end on a single invocation. Trigger: /night-park, night park, park session, me voy a dormir, guardar sesion.
testing
Interactive creator for a project feature: scaffolds the domain knowledge markdown at ai-context/features/<slug>.md AND the antenna skill at .claude/skills/<slug>/SKILL.md, both from the canonical templates. Also registers the antenna in the project's CLAUDE.md. Trigger: /feature-define <name>, define feature, documentar funcionalidad, nueva feature.