install
source · Clone the upstream repo
git clone https://github.com/Squirrelfishcityhall150/claude-code-kit
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Squirrelfishcityhall150/claude-code-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/cli/kits/nextjs/skills/nextjs" ~/.claude/skills/squirrelfishcityhall150-claude-code-kit-nextjs && rm -rf "$T"
manifest:
cli/kits/nextjs/skills/nextjs/SKILL.mdsource content
Next.js Development Guidelines
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
Core Principles
- Server-First Architecture: Default to Server Components, use Client Components only when needed
- File-Based Routing: Use App Router conventions for pages, layouts, and route handlers
- Data Fetching: Fetch data where it's needed using async/await in Server Components
- Type Safety: Leverage TypeScript for route params, search params, and data types
- Performance: Optimize with streaming, parallel data fetching, and static generation
App Router Structure
File Conventions
app/ ├── layout.tsx # Root layout (required) ├── page.tsx # Home page ├── loading.tsx # Loading UI ├── error.tsx # Error boundary ├── not-found.tsx # 404 page ├── posts/ │ ├── layout.tsx # Posts layout │ ├── page.tsx # /posts │ ├── [id]/ │ │ └── page.tsx # /posts/123 │ └── new/ │ └── page.tsx # /posts/new └── api/ └── posts/ └── route.ts # API route handler
Page Component
// app/posts/page.tsx import { getPosts } from '@/lib/api'; export const metadata = { title: 'Posts', description: 'Browse all blog posts' }; export default async function PostsPage() { const posts = await getPosts(); return ( <div> <h1>Posts</h1> <ul> {posts.map(post => ( <li key={post.id}> <a href={`/posts/${post.id}`}>{post.title}</a> </li> ))} </ul> </div> ); }
Dynamic Routes
// app/posts/[id]/page.tsx import { getPost } from '@/lib/api'; import { notFound } from 'next/navigation'; interface PageProps { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>; } export async function generateMetadata({ params }: PageProps) { const { id } = await params; const post = await getPost(id); return { title: post.title, description: post.excerpt }; } export default async function PostPage({ params }: PageProps) { const { id } = await params; const post = await getPost(id); if (!post) { notFound(); } return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ); }
Server vs Client Components
Server Components (Default)
Use for:
- Data fetching
- Accessing backend resources
- Keeping sensitive info on server
- Reducing client-side JavaScript
// app/posts/page.tsx (Server Component by default) import { db } from '@/lib/db'; export default async function PostsPage() { // Direct database access const posts = await db.post.findMany(); return <PostList posts={posts} />; }
Client Components
Use for:
- Event listeners (onClick, onChange, etc.)
- State and lifecycle (useState, useEffect)
- Browser-only APIs
- Custom hooks
// components/SearchBar.tsx 'use client'; // Required directive import { useState } from 'react'; import { useRouter } from 'next/navigation'; export function SearchBar() { const [query, setQuery] = useState(''); const router = useRouter(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); router.push(`/search?q=${query}`); }; return ( <form onSubmit={handleSubmit}> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search posts..." /> <button type="submit">Search</button> </form> ); }
Composition Pattern
// app/posts/page.tsx (Server Component) import { getPosts } from '@/lib/api'; import { SearchBar } from '@/components/SearchBar'; // Client Component export default async function PostsPage() { const posts = await getPosts(); return ( <div> <SearchBar /> {/* Client Component for interactivity */} <PostList posts={posts} /> {/* Can be Server Component */} </div> ); }
Data Fetching
Basic Pattern
// Server Component with async/await export default async function PostsPage() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 } // Cache for 1 hour }).then(res => res.json()); return <PostList posts={posts} />; }
Parallel Data Fetching
export default async function DashboardPage() { // Fetch in parallel const [user, posts, stats] = await Promise.all([ getUser(), getPosts(), getStats() ]); return ( <div> <UserProfile user={user} /> <PostList posts={posts} /> <Stats data={stats} /> </div> ); }
Streaming with Suspense
import { Suspense } from 'react'; export default function PostsPage() { return ( <div> <h1>Posts</h1> <Suspense fallback={<PostsSkeleton />}> <Posts /> </Suspense> </div> ); } async function Posts() { const posts = await getPosts(); // Slow data fetch return <PostList posts={posts} />; }
Layouts
Root Layout (Required)
// app/layout.tsx import './globals.css'; export const metadata = { title: { default: 'My Blog', template: '%s | My Blog' } }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <header> <nav>{/* Navigation */}</nav> </header> <main>{children}</main> <footer>{/* Footer */}</footer> </body> </html> ); }
Nested Layout
// app/posts/layout.tsx export default function PostsLayout({ children, }: { children: React.ReactNode; }) { return ( <div> <aside> <PostsSidebar /> </aside> <div>{children}</div> </div> ); }
Route Handlers (API Routes)
Basic Handler
// app/api/posts/route.ts import { NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { const posts = await getPosts(); return NextResponse.json({ posts }); } export async function POST(request: NextRequest) { const body = await request.json(); const post = await createPost(body); return NextResponse.json({ post }, { status: 201 }); }
Dynamic Route Handler
// app/api/posts/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const post = await getPost(params.id); if (!post) { return NextResponse.json( { error: 'Post not found' }, { status: 404 } ); } return NextResponse.json({ post }); }
Server Actions
Basic Server Action
// app/actions/posts.ts 'use server'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; const post = await db.post.create({ data: { title, content } }); revalidatePath('/posts'); redirect(`/posts/${post.id}`); }
Using in Forms
import { createPost } from '@/app/actions/posts'; export default function NewPostPage() { return ( <form action={createPost}> <input name="title" required /> <textarea name="content" required /> <button type="submit">Create Post</button> </form> ); }
Navigation
Link Component
import Link from 'next/link'; export function PostCard({ post }: { post: Post }) { return ( <Link href={`/posts/${post.id}`} prefetch={true}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </Link> ); }
Programmatic Navigation
'use client'; import { useRouter } from 'next/navigation'; export function PostActions({ postId }: { postId: string }) { const router = useRouter(); const handleDelete = async () => { await deletePost(postId); router.push('/posts'); router.refresh(); // Refresh server components }; return <button onClick={handleDelete}>Delete</button>; }
Router Hooks
'use client'; import { usePathname, useSearchParams } from 'next/navigation'; const pathname = usePathname(); // /posts/123 const searchParams = useSearchParams(); // ?q=hello const query = searchParams.get('q');
Metadata
// Static metadata export const metadata = { title: 'All Posts', description: 'Browse our collection of blog posts' }; // Dynamic metadata export async function generateMetadata({ params }: PageProps) { const { id } = await params; const post = await getPost(id); return { title: post.title, description: post.excerpt }; }
Error Handling
// app/posts/error.tsx 'use client'; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ); } // app/posts/[id]/not-found.tsx export default function NotFound() { return <div>Post Not Found</div>; }
Static Generation & ISR
// Generate static pages at build time export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ id: post.id })); } // Revalidate every hour (ISR) export const revalidate = 3600; export default async function PostPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const post = await getPost(id); return <Post data={post} />; }
Additional Resources
For detailed information, see: