Guide for implementing Next.js - a React framework for production with server-side rendering, static generation, and modern web features. Use when building Next.js applications, implementing App Router, working with server components, data fetching, routing, or optimizing performance.
62
77%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./claude/skills/nextjs/SKILL.mdNext.js is a React framework for building full-stack web applications with server-side rendering, static generation, and powerful optimization features built-in.
https://nextjs.org/docs/llms.txt
Use this skill when:
App Router (Recommended for v13+):
app/ directoryPages Router (Legacy):
pages/ directorygetStaticProps, getServerSideProps, getInitialPropsapp/ are Server Components unless marked with 'use client'npx create-next-app@latest my-app
# or
yarn create next-app my-app
# or
pnpm create next-app my-app
# or
bun create next-app my-appInteractive Setup Prompts:
src/ directory? (Optional)npm install next@latest react@latest react-dom@latestpackage.json scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}my-app/
├── app/ # App Router (v13+)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── global.css # Global styles
│ └── [folder]/ # Route segments
├── public/ # Static assets
├── components/ # React components
├── lib/ # Utility functions
├── next.config.js # Next.js configuration
├── package.json
└── tsconfig.jsonpage.tsx - Page UI for routelayout.tsx - Shared UI for segment and childrenloading.tsx - Loading UI (wraps page in Suspense)error.tsx - Error UI (wraps page in Error Boundary)not-found.tsx - 404 UIroute.ts - API endpoint (Route Handler)template.tsx - Re-rendered layout UIdefault.tsx - Parallel route fallbackStatic Route:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
└── page.tsx → /blogDynamic Route:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}Catch-all Route:
// app/shop/[...slug]/page.tsx
export default function Shop({ params }: { params: { slug: string[] } }) {
return <h1>Category: {params.slug.join('/')}</h1>
}Optional Catch-all:
// app/docs/[[...slug]]/page.tsx
// Matches /docs, /docs/a, /docs/a/b, etc.Organize routes without affecting URL:
app/
├── (marketing)/ # Group without URL segment
│ ├── about/page.tsx → /about
│ └── blog/page.tsx → /blog
└── (shop)/
├── products/page.tsx → /products
└── cart/page.tsx → /cartRender multiple pages in same layout:
app/
├── @team/ # Slot
│ └── page.tsx
├── @analytics/ # Slot
│ └── page.tsx
└── layout.tsx # Uses both slots// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
team: React.ReactNode
analytics: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}Intercept routes to show in modal:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── (..)photo/ # Intercepts /photo/[id]
└── [id]/
└── page.tsx// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<nav>Dashboard Nav</nav>
{children}
</section>
)
}Layouts are:
Components in app/ are Server Components by default:
// app/page.tsx (Server Component)
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}Benefits:
Limitations:
Mark components with 'use client' directive:
// components/counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}Use Client Components for:
// app/page.tsx (Server Component)
import { ClientComponent } from './client-component'
export default function Page() {
return (
<div>
<h1>Server-rendered content</h1>
<ClientComponent />
</div>
)
}// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
})
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}Force Cache (Default):
fetch('https://api.example.com/data', { cache: 'force-cache' })No Store (Dynamic):
fetch('https://api.example.com/data', { cache: 'no-store' })Revalidate:
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Seconds
})Tag-based Revalidation:
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// Revalidate elsewhere:
import { revalidateTag } from 'next/cache'
revalidateTag('posts')async function getData() {
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
])
return { posts, users }
}async function getData() {
const post = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json())
const author = await fetch(`https://api.example.com/users/${post.authorId}`).then(r => r.json())
return { post, author }
}// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello' })
}
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body })
}// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
return Response.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await deletePost(params.id)
return new Response(null, { status: 204 })
}export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const cookies = request.headers.get('cookie')
return Response.json({ id })
}// JSON
return Response.json({ data: 'value' })
// Text
return new Response('Hello', { headers: { 'Content-Type': 'text/plain' } })
// Redirect
return Response.redirect('https://example.com')
// Status codes
return new Response('Not Found', { status: 404 })import Link from 'next/link'
export default function Page() {
return (
<>
<Link href="/about">About</Link>
<Link href="/blog/post-1">Post 1</Link>
<Link href={{ pathname: '/blog/[slug]', query: { slug: 'post-1' } }}>
Post 1 (alternative)
</Link>
</>
)
}'use client'
import { useRouter } from 'next/navigation'
export function NavigateButton() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}Router Methods:
router.push(href) - Navigate to routerouter.replace(href) - Replace current historyrouter.refresh() - Refresh current routerouter.back() - Navigate backrouter.forward() - Navigate forwardrouter.prefetch(href) - Prefetch routeimport { redirect } from 'next/navigation'
export default async function Page() {
const session = await getSession()
if (!session) {
redirect('/login')
}
return <div>Protected content</div>
}// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description',
keywords: ['nextjs', 'react'],
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My Page',
description: 'Page description',
images: ['/twitter-image.jpg'],
},
}
export default function Page() {
return <div>Content</div>
}// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}favicon.ico, icon.png, apple-icon.png - Faviconsopengraph-image.png, twitter-image.png - Social imagesrobots.txt - Robots filesitemap.xml - Sitemapimport Image from 'next/image'
export default function Page() {
return (
<>
{/* Local image */}
<Image
src="/profile.png"
alt="Profile"
width={500}
height={500}
/>
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={500}
height={500}
/>
{/* Responsive fill */}
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/hero.jpg"
alt="Hero"
fill
style={{ objectFit: 'cover' }}
/>
</div>
{/* Priority loading */}
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
</>
)
}Image Props:
src - Image path (local or URL)alt - Alt text (required)width, height - Dimensions (required unless fill)fill - Fill parent containersizes - Responsive sizesquality - 1-100 (default 75)priority - Preload imageplaceholder - 'blur' | 'empty'blurDataURL - Data URL for blur// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
pathname: '/images/**',
},
],
},
}// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>
}// app/page.tsx
import { Suspense } from 'react'
async function Posts() {
const posts = await getPosts()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
export default function Page() {
return (
<div>
<h1>My Posts</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
</div>
)
}// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}// app/global-error.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>
)
}// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Not Found</h2>
<p>Could not find requested resource</p>
</div>
)
}
// Trigger programmatically
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const post = await getPost(params.id)
if (!post) {
notFound()
}
return <div>{post.title}</div>
}// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add custom header
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com// Server-side only
const dbUrl = process.env.DATABASE_URL
// Client and server (NEXT_PUBLIC_ prefix)
const apiUrl = process.env.NEXT_PUBLIC_API_URL/** @type {import('next').NextConfig} */
const nextConfig = {
// React strict mode
reactStrictMode: true,
// Image domains
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'example.com' },
],
},
// Redirects
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
]
},
// Rewrites
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
},
// Headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
]
},
// Environment variables
env: {
CUSTOM_KEY: 'value',
},
}
module.exports = nextConfignext/image for automatic optimizationpriority for above-fold images, lazy load below-fold// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
export default async function DashboardLayout({ children }) {
const session = await getSession()
if (!session) {
redirect('/login')
}
return <>{children}</>
}// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title')
await db.post.create({ data: { title } })
revalidatePath('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<button type="submit">Create</button>
</form>
)
}// Generate static params for dynamic routes
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
export default async function Post({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}# Install Vercel CLI
npm i -g vercel
# Deploy
vercel# Build
npm run build
# Start production server
npm startRequirements:
output: 'standalone' in next.config.js (optional, reduces size)FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Hydration errors
Images not loading
next.config.js/ for public)API route 404
route.ts/js not index.tsapp/api/ directory"use client" errors
'use client' to components using hooks'use client'Metadata not updating
When building with Next.js:
create-next-appb1b2fe0
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.