Requirements
- Target platform
- OpenClaw
- Install method
- Manual import
- Extraction
- Extract archive
- Prerequisites
- OpenClaw
- Primary doc
- SKILL.md
Build, optimize, and operate production Next.js apps with best practices for architecture, data fetching, caching, rendering, testing, deployment, and observ...
Build, optimize, and operate production Next.js apps with best practices for architecture, data fetching, caching, rendering, testing, deployment, and observ...
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete.
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run.
Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies β everything beyond "hello world."
Run through these 8 signals β score 0 (no) or 2 (yes): SignalCheckScoreποΈ ArchitectureServer/Client Component boundary is intentional, not accidental/2β‘ PerformanceCore Web Vitals all green (LCP <2.5s, INP <200ms, CLS <0.1)/2π SecurityNo secrets in client bundles, CSP headers configured/2π¦ BundleNo unnecessary client JS, tree-shaking working/2ποΈ DataCaching strategy defined (not just defaults)/2π§ͺ TestingE2E + unit tests in CI, >70% coverage on critical paths/2π DeployPreview deploys, rollback capability, monitoring/2π ObservabilityError tracking, performance monitoring, structured logging/2 Score: /16 β 14-16 Production-ready | 10-13 Needs work | <10 Risk zone
Default: App Router for all new projects (Next.js 13.4+). Use Pages Router ONLY if: Migrating existing Pages Router app (incremental adoption) Team has zero RSC experience AND shipping deadline <2 weeks Library dependency requires Pages Router patterns
src/ βββ app/ # App Router β routes only β βββ (auth)/ # Route group β shared auth layout β β βββ login/page.tsx β β βββ register/page.tsx β βββ (dashboard)/ # Route group β shared dashboard layout β β βββ layout.tsx β β βββ page.tsx β β βββ settings/page.tsx β βββ api/ # Route Handlers (use sparingly) β β βββ webhooks/ β β βββ stripe/route.ts β βββ layout.tsx # Root layout β βββ loading.tsx # Root loading β βββ error.tsx # Root error boundary β βββ not-found.tsx # 404 page β βββ global-error.tsx # Global error boundary βββ components/ # Shared components β βββ ui/ # Design system primitives β βββ forms/ # Form components β βββ layouts/ # Layout components βββ lib/ # Shared utilities β βββ db/ # Database client & queries β βββ auth/ # Auth utilities β βββ api/ # External API clients β βββ utils/ # Pure utility functions βββ hooks/ # Custom React hooks (client-only) βββ actions/ # Server Actions βββ types/ # TypeScript types βββ styles/ # Global styles βββ config/ # App configuration
Routes are thin β page.tsx imports components, doesn't contain business logic Components are reusable β never import from app/ into components/ Server Actions get their own directory β organized by domain, not by page No barrel files (index.ts re-exports) β they break tree-shaking Colocation for route-specific β _components/ in route folders for non-shared components
ScenarioStrategyWhyStatic content (blog, docs, marketing)Static (SSG)Build-time generation, CDN-cachedUser-specific dashboardDynamic ServerFresh data per requestProduct listing with pricesISR (revalidate: 3600)Fresh enough, fast deliveryReal-time data (chat, stocks)Client-side + WebSocketServer can't push updatesSEO-critical + fresh dataDynamic Server + streamingFast TTFB with SuspenseHighly interactive form/wizardClient ComponentComplex state management
DEFAULT: Server Component (every .tsx is server by default) Add "use client" ONLY when you need: β useState, useEffect, useRef, useContext β Browser APIs (window, document, localStorage) β Event handlers (onClick, onChange, onSubmit) β Third-party client libraries (framer-motion, react-hook-form) NEVER add "use client" because: β You want to use async/await (Server Components support this natively) β You're fetching data (fetch in Server Components, not useEffect) β You're importing a server-only library β "It's not working" β debug the actual issue first
// β CORRECT: Server Component wraps Client Component // app/dashboard/page.tsx (Server Component) import { getUser } from '@/lib/auth' import { DashboardClient } from './_components/dashboard-client' export default async function DashboardPage() { const user = await getUser() // Server-side data fetch return <DashboardClient user={user} /> // Pass as props } // _components/dashboard-client.tsx 'use client' export function DashboardClient({ user }: { user: User }) { const [tab, setTab] = useState('overview') return <div>...</div> } Push "use client" as far down the tree as possible. The boundary should be at the leaf, not the root.
Server Component direct fetch β simplest, most performant Server Actions β for mutations and form submissions Route Handlers β for webhooks, external API endpoints Client-side fetch (SWR/React Query) β for real-time/polling data only
// Static data (cached indefinitely, revalidated on deploy) const data = await fetch('https://api.example.com/data', { cache: 'force-cache' // Default in App Router }) // Revalidate every hour const data = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } }) // Always fresh (no cache) const data = await fetch('https://api.example.com/data', { cache: 'no-store' }) // Tag-based revalidation const data = await fetch('https://api.example.com/products', { next: { tags: ['products'] } }) // Then in a Server Action: import { revalidateTag } from 'next/cache' revalidateTag('products')
Data TypeCache StrategyRevalidateTagsCMS contentISR3600s (1h)['cms', 'posts']Product catalogISR300s (5m)['products']User profileNo cacheββPricing/inventoryNo cacheββStatic assetsForce cacheOn deployβAnalytics/dashboardsISR60s['analytics']Auth tokensNo cacheββ
import { unstable_cache } from 'next/cache' import { db } from '@/lib/db' // Cache database queries with tags const getProducts = unstable_cache( async (categoryId: string) => { return db.query.products.findMany({ where: eq(products.categoryId, categoryId) }) }, ['products'], // Cache key parts { revalidate: 300, tags: ['products'] } )
// β CORRECT: Parallel fetches export default async function DashboardPage() { const [user, stats, notifications] = await Promise.all([ getUser(), getStats(), getNotifications() ]) return <Dashboard user={user} stats={stats} notifications={notifications} /> } // β WRONG: Sequential waterfall export default async function DashboardPage() { const user = await getUser() const stats = await getStats(user.id) // Waits for user const notifications = await getNotifications(user.id) // Waits for stats }
import { Suspense } from 'react' export default async function Page() { return ( <div> <h1>Dashboard</h1> {/* Fast: renders immediately */} <UserGreeting /> {/* Slow: streams in when ready */} <Suspense fallback={<StatsSkeleton />}> <StatsPanel /> {/* Async Server Component */} </Suspense> <Suspense fallback={<FeedSkeleton />}> <ActivityFeed /> </Suspense> </div> ) }
// actions/user.ts 'use server' import { z } from 'zod' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' const updateProfileSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), bio: z.string().max(500).optional() }) export async function updateProfile(formData: FormData) { // 1. Authenticate const session = await getSession() if (!session) throw new Error('Unauthorized') // 2. Validate const parsed = updateProfileSchema.safeParse({ name: formData.get('name'), email: formData.get('email'), bio: formData.get('bio') }) if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors } } // 3. Authorize if (session.userId !== formData.get('userId')) { throw new Error('Forbidden') } // 4. Mutate await db.update(users) .set(parsed.data) .where(eq(users.id, session.userId)) // 5. Revalidate revalidatePath('/profile') return { success: true } }
Always validate input β FormData is user input, never trust it Always check auth β Server Actions are public endpoints Always check authorization β user can only modify their own data Use Zod for validation β type-safe, composable schemas Return errors, don't throw β throwing shows error boundary, returning shows inline errors Revalidate after mutations β revalidatePath or revalidateTag Never return sensitive data β return only what the client needs
'use client' import { useActionState } from 'react' import { updateProfile } from '@/actions/user' export function ProfileForm({ user }: { user: User }) { const [state, action, pending] = useActionState(updateProfile, null) return ( <form action={action}> <input name="name" defaultValue={user.name} /> {state?.error?.name && <p className="text-red-500">{state.error.name}</p>} <button type="submit" disabled={pending}> {pending ? 'Saving...' : 'Save'} </button> {state?.success && <p className="text-green-500">Saved!</p>} </form> ) }
MethodBest ForLibrariesSession-based (cookie)Traditional web appsNextAuth.js / Auth.jsJWTAPI-first, mobile clientsjose, customOAuth onlySocial login, quick startNextAuth.jsPasskeys/WebAuthnModern, passwordlessSimpleWebAuthnThird-partyEnterprise, complianceClerk, Auth0, Supabase Auth
// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' const publicRoutes = ['/', '/login', '/register', '/api/webhooks'] const authRoutes = ['/login', '/register'] export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const token = request.cookies.get('session')?.value // Public routes β allow if (publicRoutes.some(route => pathname.startsWith(route))) { // Redirect authenticated users away from auth pages if (token && authRoutes.some(route => pathname.startsWith(route))) { return NextResponse.redirect(new URL('/dashboard', request.url)) } return NextResponse.next() } // Protected routes β require auth if (!token) { const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('callbackUrl', pathname) return NextResponse.redirect(loginUrl) } return NextResponse.next() } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'] }
// lib/auth/permissions.ts type Permission = 'read' | 'write' | 'admin' type Resource = 'posts' | 'users' | 'settings' const rolePermissions: Record<string, Record<Resource, Permission[]>> = { admin: { posts: ['read', 'write', 'admin'], users: ['read', 'write', 'admin'], settings: ['read', 'write', 'admin'] }, editor: { posts: ['read', 'write'], users: ['read'], settings: ['read'] }, viewer: { posts: ['read'], users: [], settings: [] } } export function can(role: string, resource: Resource, permission: Permission): boolean { return rolePermissions[role]?.[resource]?.includes(permission) ?? false } // Usage in Server Component export default async function AdminPage() { const session = await getSession() if (!can(session.role, 'settings', 'admin')) { notFound() // Don't reveal admin pages exist } return <AdminDashboard /> }
const securityHeaders = [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, { key: 'Content-Security-Policy', value: ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; `.replace(/\n/g, '') } ]
MetricGoodNeeds ImprovementPoorLCP<2.5s2.5-4.0s>4.0sINP<200ms200-500ms>500msCLS<0.10.1-0.25>0.25TTFB<800ms800ms-1.8s>1.8sFCP<1.8s1.8-3.0s>3.0s
import Image from 'next/image' // β Always use next/image <Image src="/hero.jpg" alt="Hero image" width={1200} height={630} priority // LCP image β load immediately sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" placeholder="blur" blurDataURL={shimmer} // Base64 placeholder /> // For dynamic images <Image src={user.avatar} alt={user.name} width={48} height={48} loading="lazy" // Below fold β lazy load />
Always set priority on LCP image (hero, above-fold) Always provide sizes β prevents downloading oversized images Use placeholder="blur" for large images β prevents CLS Configure remotePatterns in next.config.ts for external images Use WebP/AVIF β next/image auto-converts by default
// next.config.ts const nextConfig = { // Strict mode for catching bugs reactStrictMode: true, // Optimize packages experimental: { optimizePackageImports: [ 'lucide-react', '@radix-ui/react-icons', 'date-fns', 'lodash-es' ] }, // Bundle analyzer (dev only) // npm install @next/bundle-analyzer ...(process.env.ANALYZE === 'true' && { webpack: (config) => { const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') config.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static' })) return config } }) }
import dynamic from 'next/dynamic' // Heavy chart library β only load when needed const Chart = dynamic(() => import('@/components/chart'), { loading: () => <ChartSkeleton />, ssr: false // Client-only component }) // Code editor β definitely client-only const CodeEditor = dynamic(() => import('@/components/code-editor'), { ssr: false })
// app/layout.tsx import { Inter, JetBrains_Mono } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter' }) const jetbrains = JetBrains_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-mono' }) export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.variable} ${jetbrains.variable}`}> <body className="font-sans">{children}</body> </html> ) }
ResourceBudgetToolFirst Load JS<100KBnext build outputPage JS<50KB per routeBundle analyzerTotal page weight<500KBLighthouseLCP image<200KBnext/image handlesThird-party scripts<50KB totalScript componentWeb fonts<100KBnext/font handles
ORMBest ForTradeoffsDrizzleType-safe, lightweight, SQL-likeNewer ecosystemPrismaRapid prototyping, schema-firstHeavier, edge limitationsKyselyType-safe raw SQLMore manual, no migrationsRaw SQL (pg/mysql2)Max performance, full controlNo type safety, manual migrations
// lib/db/index.ts import { drizzle } from 'drizzle-orm/node-postgres' import { Pool } from 'pg' import * as schema from './schema' const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, idleTimeoutMillis: 30000 }) export const db = drizzle(pool, { schema }) // lib/db/schema.ts import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), email: text('email').notNull().unique(), name: text('name').notNull(), role: text('role', { enum: ['admin', 'editor', 'viewer'] }).default('viewer'), emailVerified: boolean('email_verified').default(false), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow() })
// For Vercel/serverless β use connection pooler // Neon: use pooler URL (port 5432 β 6543) // Supabase: use Supavisor URL // PlanetScale: serverless driver built-in // lib/db/index.ts (serverless-safe) import { neon } from '@neondatabase/serverless' import { drizzle } from 'drizzle-orm/neon-http' const sql = neon(process.env.DATABASE_URL!) export const db = drizzle(sql)
LevelToolWhat to TestCoverage TargetUnitVitestUtils, hooks, pure functions80%+ComponentTesting Library + VitestUI components, forms70%+IntegrationTesting LibraryPage-level with mocked dataKey flowsE2EPlaywrightCritical user journeys5-10 flowsVisualPlaywright screenshotsUI regressionKey pages
// vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { environment: 'jsdom', setupFiles: ['./tests/setup.ts'], include: ['**/*.test.{ts,tsx}'], coverage: { provider: 'v8', reporter: ['text', 'lcov'], exclude: ['**/*.config.*', '**/types/**'] } } })
// Server Components can be tested as async functions import { render } from '@testing-library/react' import Page from '@/app/dashboard/page' // Mock the data fetching vi.mock('@/lib/db', () => ({ getUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }) })) test('dashboard page renders user name', async () => { const Component = await Page() // Call as async function const { getByText } = render(Component) expect(getByText('Test')).toBeInTheDocument() })
// e2e/auth.spec.ts import { test, expect } from '@playwright/test' test.describe('Authentication', () => { test('login flow', async ({ page }) => { await page.goto('/login') await page.fill('[name="email"]', 'test@example.com') await page.fill('[name="password"]', 'password123') await page.click('button[type="submit"]') await expect(page).toHaveURL('/dashboard') await expect(page.getByText('Welcome')).toBeVisible() }) test('protected route redirects', async ({ page }) => { await page.goto('/dashboard') await expect(page).toHaveURL(/\/login/) }) })
app/ βββ global-error.tsx # Catches root layout errors (must include <html>) βββ error.tsx # Catches app-level errors βββ not-found.tsx # 404 page βββ (dashboard)/ β βββ error.tsx # Dashboard-specific errors β βββ settings/ β βββ error.tsx # Settings-specific errors
// app/error.tsx 'use client' import { useEffect } from 'react' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { // Log to error tracking service console.error('Application error:', error) // Sentry.captureException(error) }, [error]) return ( <div className="flex flex-col items-center justify-center min-h-[400px]"> <h2 className="text-2xl font-bold">Something went wrong</h2> <p className="text-gray-500 mt-2"> {error.digest ? `Error ID: ${error.digest}` : error.message} </p> <button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" > Try again </button> </div> ) }
// lib/logger.ts type LogLevel = 'debug' | 'info' | 'warn' | 'error' function log(level: LogLevel, message: string, meta?: Record<string, unknown>) { const entry = { timestamp: new Date().toISOString(), level, message, ...meta, // Add request context if available ...(meta?.requestId && { requestId: meta.requestId }) } if (level === 'error') { console.error(JSON.stringify(entry)) } else { console.log(JSON.stringify(entry)) } } export const logger = { debug: (msg: string, meta?: Record<string, unknown>) => log('debug', msg, meta), info: (msg: string, meta?: Record<string, unknown>) => log('info', msg, meta), warn: (msg: string, meta?: Record<string, unknown>) => log('warn', msg, meta), error: (msg: string, meta?: Record<string, unknown>) => log('error', msg, meta) }
PlatformBest ForEdgeDBCost (hobby)VercelDefault choice, best DXβ ExternalFree β $20/moCloudflare PagesEdge-first, Workersβ D1, KVFree β $5/moAWS AmplifyAWS ecosystemβ RDS, DynamoDBPay-per-useRailwayFull-stack, DockerβBuilt-in Postgres$5/moFly.ioGlobal, Dockerβ Built-in PostgresPay-per-useSelf-hosted (Docker)Full controlβAnyServer cost
# Dockerfile FROM node:20-alpine AS base RUN corepack enable FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN pnpm build FROM base AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] // next.config.ts β required for standalone const nextConfig = { output: 'standalone' }
# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm tsc --noEmit - run: pnpm lint - run: pnpm test -- --coverage - run: pnpm build e2e: runs-on: ubuntu-latest needs: quality steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm exec playwright install --with-deps - run: pnpm build - run: pnpm exec playwright test - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/
// env.ts β runtime validation with t3-env import { createEnv } from '@t3-oss/env-nextjs' import { z } from 'zod' export const env = createEnv({ server: { DATABASE_URL: z.string().url(), AUTH_SECRET: z.string().min(32), STRIPE_SECRET_KEY: z.string().startsWith('sk_'), REDIS_URL: z.string().url().optional(), }, client: { NEXT_PUBLIC_APP_URL: z.string().url(), NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, AUTH_SECRET: process.env.AUTH_SECRET, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, REDIS_URL: process.env.REDIS_URL, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_STRIPE_KEY: process.env.NEXT_PUBLIC_STRIPE_KEY, }, })
'use client' import { useOptimistic, useTransition } from 'react' import { toggleTodo } from '@/actions/todos' export function TodoItem({ todo }: { todo: Todo }) { const [optimisticTodo, setOptimisticTodo] = useOptimistic(todo) const [, startTransition] = useTransition() return ( <label> <input type="checkbox" checked={optimisticTodo.completed} onChange={() => { startTransition(async () => { setOptimisticTodo({ ...todo, completed: !todo.completed }) await toggleTodo(todo.id) }) }} /> {optimisticTodo.title} </label> ) }
'use client' import { useInView } from 'react-intersection-observer' import { useEffect, useState, useTransition } from 'react' import { loadMore } from '@/actions/feed' export function InfiniteList({ initialItems }: { initialItems: Item[] }) { const [items, setItems] = useState(initialItems) const [cursor, setCursor] = useState(initialItems.at(-1)?.id) const [hasMore, setHasMore] = useState(true) const [isPending, startTransition] = useTransition() const { ref, inView } = useInView() useEffect(() => { if (inView && hasMore && !isPending) { startTransition(async () => { const newItems = await loadMore(cursor) if (newItems.length === 0) { setHasMore(false) } else { setItems(prev => [...prev, ...newItems]) setCursor(newItems.at(-1)?.id) } }) } }, [inView, hasMore, isPending, cursor]) return ( <div> {items.map(item => <ItemCard key={item.id} item={item} />)} {hasMore && <div ref={ref}>{isPending ? <Spinner /> : null}</div>} </div> ) }
'use client' import { useRouter, useSearchParams, usePathname } from 'next/navigation' import { useDebouncedCallback } from 'use-debounce' export function SearchBar() { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const handleSearch = useDebouncedCallback((term: string) => { const params = new URLSearchParams(searchParams) if (term) { params.set('q', term) params.set('page', '1') } else { params.delete('q') } router.replace(`${pathname}?${params.toString()}`) }, 300) return ( <input type="search" placeholder="Search..." defaultValue={searchParams.get('q') ?? ''} onChange={e => handleSearch(e.target.value)} /> ) }
// app/onboarding/page.tsx export default function OnboardingPage({ searchParams }: { searchParams: { step?: string } }) { const step = Number(searchParams.step) || 1 return ( <div> <ProgressBar step={step} total={4} /> {step === 1 && <StepOne />} {step === 2 && <StepTwo />} {step === 3 && <StepThree />} {step === 4 && <StepFour />} </div> ) }
next build succeeds with zero warnings TypeScript strict mode, no any types in production code All environment variables validated (t3-env or manual) Security headers configured (CSP, HSTS, X-Frame-Options) Authentication + authorization tested (pen test critical flows) Error boundaries at every route level 404 and 500 pages customized Favicon, OG images, meta tags configured Core Web Vitals passing (Lighthouse >90) Mobile responsive tested on real devices Accessibility audit (axe, keyboard nav, screen reader) Rate limiting on API routes and Server Actions CORS configured correctly Database connection pooling configured for serverless Monitoring & error tracking connected (Sentry, etc.)
E2E tests for critical user journeys Bundle size within budget (<100KB first load) Image optimization verified (next/image, proper sizes) Sitemap.xml and robots.txt configured Analytics configured Preview deployment tested Rollback plan documented Load testing completed CDN caching verified Edge middleware tested in production environment
#MistakeFix1"use client" at the top of every fileDefault to Server Components, push client boundary down2Fetching data with useEffectFetch in Server Components or use SWR/React Query for client3Not using loading.tsxAdd loading states to prevent layout shift4Ignoring bundle sizeRun next build and check output, use dynamic imports5No error boundariesAdd error.tsx at every route level6Storing secrets in NEXT_PUBLIC_*Server-only env vars for secrets, validate with t3-env7Not setting image sizes propAlways provide sizes for responsive images8Sequential data fetchingUse Promise.all() for parallel fetches9Caching everything or nothingExplicit cache strategy per data type10Not using revalidateTagTag-based revalidation for precise cache control
Build error? βββ "Module not found" β Check import paths, tsconfig paths βββ "Server Component error" β Remove "use client" or move hooks to client component βββ "Hydration mismatch" β Check for browser-only code in shared components β β Use suppressHydrationWarning for timestamps β β Wrap in useEffect or dynamic(ssr: false) βββ "Edge runtime error" β Check node APIs (fs, crypto) not available at edge βββ Slow build β Check for large static generation, reduce ISR pages Runtime error? βββ 500 on production β Check error.tsx, logs, Sentry βββ Slow TTFB β Check database queries, add caching βββ CLS β Add explicit dimensions to images/embeds βββ High JS bundle β Run bundle analyzer, dynamic import heavy libs βββ Stale data β Check revalidation settings, revalidateTag
LayerRecommendationWhyFrameworkNext.js 15+ (App Router)RSC, streaming, Server ActionsLanguageTypeScript (strict)Type safety, better DXStylingTailwind CSS 4Utility-first, no runtime costUI Componentsshadcn/uiCopy-paste, customizableFormsreact-hook-form + zodType-safe validationORMDrizzleType-safe, lightweight, SQL-likeDatabasePostgreSQL (Neon/Supabase)Serverless-friendly, provenAuthAuth.js (NextAuth v5)Built for Next.jsPaymentsStripeIndustry standardHostingVercelBest Next.js DXTestingVitest + PlaywrightFast unit + reliable E2EMonitoringSentryError tracking + performanceAnalyticsPostHogProduct analytics, open source
DimensionWeightScoringArchitecture (RSC boundaries, structure)20%0-20Performance (CWV, bundle, TTFB)20%0-20Security (auth, headers, validation)15%0-15Data layer (caching, fetching, DB)15%0-15Testing (pyramid, coverage, E2E)10%0-10Error handling (boundaries, logging)10%0-10DX (types, linting, CI)5%0-5Deployment (Docker/platform, monitoring)5%0-5 Score: 90-100 Elite | 75-89 Production-ready | 60-74 Needs improvement | <60 Not production-ready
"Set up a new Next.js project" β Phase 1 architecture + structure + Phase 6 DB setup "Add authentication" β Phase 4 auth pattern + middleware + authorization "Optimize performance" β Phase 5 full checklist + image + bundle + fonts "Set up testing" β Phase 7 full pyramid + Vitest + Playwright config "Deploy to production" β Phase 9 platform selection + Docker + CI/CD + env vars "Fix hydration error" β Phase 12 troubleshooting tree "Add caching" β Phase 2 caching strategy table + fetch config + tags "Create a Server Action" β Phase 3 best practices + useActionState pattern "Audit my app" β Quick health check + Phase 11 production checklist "Add error handling" β Phase 8 error boundary architecture + logging "Set up search" β Phase 10 search with URL state pattern "Review my architecture" β Phase 1 decision matrix + rendering strategy Built by AfrexAI β the AI automation agency that ships. Zero dependencies.
Code helpers, APIs, CLIs, browser automation, testing, and developer operations.
Largest current source with strong distribution and engagement signals.