ai-templates
Version:
CLI tool for generating AI templates for different frameworks and tools
1,460 lines (1,238 loc) • 34.2 kB
Markdown
This file provides guidance to Claude Code (claude.ai/code) when working with code in this Next.js frontend project.
## Project Overview
A modern, production-ready Next.js 14+ application built with TypeScript, featuring:
- App Router architecture for improved performance and DX
- Server and Client Components optimization
- Internationalization (i18n) support
- SEO and accessibility first approach
- Comprehensive testing strategy (Jest + Playwright)
- Storybook for component development
- Performance monitoring and optimization
## Tech Stack
### Core
- **Framework**: Next.js 14+ (App Router)
- **Language**: TypeScript 5+
- **Styling**: Tailwind CSS + CSS Modules
- **State Management**: Zustand / React Context
- **Data Fetching**: React Query + fetch
### Development
- **Component Development**: Storybook 7+
- **Testing**: Jest + React Testing Library + Playwright
- **Linting**: ESLint + Prettier + Husky
- **Commit Convention**: Commitlint with Conventional Commits
- **Package Manager**: pnpm (preferred) / npm / yarn
### Infrastructure
- **Deployment**: Vercel / Docker
- **CDN**: Cloudflare / Vercel Edge Network
- **Analytics**: Vercel Analytics + Google Analytics
- **Monitoring**: Sentry + Vercel Speed Insights
## Environment Setup
### Prerequisites
```bash
# Required
- Node.js 18.17+ (use nvm)
- pnpm 8+ (npm install -g pnpm)
- Git
# Optional but recommended
- Docker Desktop
- VS Code with recommended extensions
```
### Initial Setup
```bash
# Clone repository
git clone <repository-url>
cd <project-name>
# Install dependencies
pnpm install
# Setup environment
cp .env.example .env.local
# Run development server
pnpm dev
# Open browser
open http://localhost:3000
```
### Environment Files
```
.env.example # Template with all variables (committed)
.env.local # Local development (gitignored)
.env.test # Test environment
.env.production # Production values (CI/CD managed)
```
## Directory Structure
```
src/
├── app/ # App Router pages and layouts
│ ├── (auth)/ # Route groups
│ ├── api/ # API routes
│ ├── [locale]/ # i18n dynamic routes
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Home page
│ │ └── (routes)/ # Other pages
│ ├── error.tsx # Error boundary
│ ├── not-found.tsx # 404 page
│ └── global-error.tsx # Global error boundary
├── components/ # React components
│ ├── ui/ # Base UI components
│ ├── features/ # Feature-specific components
│ ├── layouts/ # Layout components
│ └── providers/ # Context providers
├── hooks/ # Custom React hooks
├── lib/ # Utilities and configs
│ ├── api/ # API client utilities
│ ├── auth/ # Authentication helpers
│ ├── db/ # Database utilities
│ └── utils/ # General utilities
├── services/ # Business logic services
├── stores/ # Zustand stores
├── styles/ # Global styles
├── types/ # TypeScript types
├── locales/ # i18n translations
└── config/ # App configuration
├── env.ts # Environment validation
├── site.ts # Site metadata
└── features.ts # Feature toggles
```
## App Router Guidelines
### File Conventions
```typescript
// app/[locale]/products/page.tsx
import { Metadata } from 'next'
// Metadata generation
export async function generateMetadata({ params }): Promise<Metadata> {
return {
title: 'Products',
description: 'Browse our products'
}
}
// Static params generation
export async function generateStaticParams() {
return [
{ locale: 'en' },
{ locale: 'es' },
{ locale: 'fr' }
]
}
// Page component
export default async function ProductsPage({ params, searchParams }) {
const products = await getProducts(searchParams)
return <ProductList products={products} />
}
```
```
app/
├── (marketing)/
│ ├── about/
│ └── contact/
├── (shop)/
│ ├── products/
│ └── cart/
├── (auth)/
│ ├── login/
│ └── register/
└── dashboard/
├── layout.tsx
└── settings/
```
```typescript
// app/components/ProductList.tsx
// This is a Server Component by default
import { db } from '@/lib/db'
async function ProductList({ category }: { category: string }) {
// Direct database access in Server Components
const products = await db.product.findMany({
where: { category },
select: {
id: true,
name: true,
price: true,
image: true
}
})
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// Server Component composition
export default async function ProductsSection() {
return (
<>
<ProductList category="featured" />
<Suspense fallback={<ProductsSkeleton />}>
<ProductList category="new" />
</Suspense>
</>
)
}
```
```typescript
// Deduped fetch requests
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`, {
next: { revalidate: 3600 }, // Cache for 1 hour
cache: 'force-cache' // or 'no-store' for dynamic
})
if (!res.ok) throw new Error('Failed to fetch user')
return res.json()
}
```
```typescript
'use client'
// components/InteractiveChart.tsx
import { useState, useEffect } from 'react'
import { Chart } from '@/components/ui/Chart'
export function InteractiveChart({ data }) {
const [filter, setFilter] = useState('all')
// Client-side interactivity
useEffect(() => {
// Browser APIs
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return (
<div>
<FilterButtons value={filter} onChange={setFilter} />
<Chart data={filterData(data, filter)} />
</div>
)
}
```
```typescript
// Server Component
async function ProductPage({ id }: { id: string }) {
const product = await getProduct(id)
return (
<>
<ProductDetails product={product} />
{/* Client Component for interactivity */}
<AddToCartButton productId={id} />
</>
)
}
// Client Component
'use client'
function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false)
const handleAddToCart = async () => {
setLoading(true)
await addToCart(productId)
setLoading(false)
}
return (
<button onClick={handleAddToCart} disabled={loading}>
{loading ? 'Adding...' : 'Add to Cart'}
</button>
)
}
```
```typescript
// components/ui/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
const meta = {
title: 'UI/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger']
}
}
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
}
export const WithIcon: Story = {
args: {
variant: 'secondary',
children: 'Download',
icon: <DownloadIcon />,
},
}
// Interaction testing
export const Clickable: Story = {
args: {
children: 'Click me',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)
const button = await canvas.getByRole('button')
await userEvent.click(button)
await expect(button).toHaveFocus()
},
}
```
```javascript
// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-interactions',
'storybook-addon-next',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
}
```
```typescript
import Image from 'next/image'
// Static import with blur placeholder
import heroImage from '@/public/hero.jpg'
export function Hero() {
return (
<Image
src={heroImage}
alt="Hero image"
priority // Load immediately
placeholder="blur" // Show blur while loading
quality={90} // Higher quality for hero
/>
)
}
// Dynamic images with proper sizing
export function ProductImage({ src, alt }) {
return (
<div className="relative aspect-square">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
</div>
)
}
// Responsive images
export function ResponsiveImage({ desktop, mobile, alt }) {
return (
<picture>
<source media="(min-width: 768px)" srcSet={desktop} />
<Image src={mobile} alt={alt} width={800} height={600} />
</picture>
)
}
```
```javascript
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
}
```
```typescript
// config/i18n.ts
export const locales = ['en', 'es', 'fr', 'de'] as const
export const defaultLocale = 'en' as const
export type Locale = (typeof locales)[number]
// i18n routing
export const i18n = {
locales,
defaultLocale,
}
```
```typescript
// middleware.ts
import { NextRequest } from 'next/server'
import { i18n } from '@/config/i18n'
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Check if locale is in pathname
const pathnameHasLocale = i18n.locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// Redirect to default locale
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
return Response.redirect(request.nextUrl)
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
```
```typescript
// hooks/useTranslation.ts
'use client'
import { useParams } from 'next/navigation'
import { translations } from '@/locales'
export function useTranslation() {
const params = useParams()
const locale = params.locale as Locale || 'en'
const t = (key: string) => {
return translations[locale][key] || key
}
return { t, locale }
}
```
```typescript
// app/[locale]/products/[id]/page.tsx
import { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id)
return {
title: `${product.name} | My Store`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [
{
url: product.image,
width: 1200,
height: 630,
alt: product.name,
}
],
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.image],
},
alternates: {
canonical: `/products/${product.id}`,
languages: {
'en': `/en/products/${product.id}`,
'es': `/es/products/${product.id}`,
},
},
}
}
```
```typescript
// components/ProductSchema.tsx
export function ProductSchema({ product }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.images,
sku: product.sku,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
},
review: product.reviews.map(review => ({
'@type': 'Review',
reviewRating: {
'@type': 'Rating',
ratingValue: review.rating,
},
author: {
'@type': 'Person',
name: review.author,
},
})),
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
)
}
```
```typescript
// components/ui/Modal.tsx
'use client'
import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
interface ModalProps {
isOpen: boolean
onClose: () => void
title: string
children: React.ReactNode
}
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
const closeButtonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (isOpen) {
// Focus trap
closeButtonRef.current?.focus()
// Prevent body scroll
document.body.style.overflow = 'hidden'
// Handle escape key
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleEscape)
return () => {
document.body.style.overflow = ''
document.removeEventListener('keydown', handleEscape)
}
}
}, [isOpen, onClose])
if (!isOpen) return null
return createPortal(
<div
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
className="fixed inset-0 z-50"
>
<div
className="bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
<div className="fixed inset-0 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<h2 id="modal-title" className="text-xl font-bold mb-4">
{title}
</h2>
{children}
<button
ref={closeButtonRef}
onClick={onClose}
className="mt-4 px-4 py-2 bg-gray-200 rounded"
aria-label="Close modal"
>
Close
</button>
</div>
</div>
</div>,
document.body
)
}
```
### Automated Testing
```typescript
// components/ui/Button/Button.test.tsx
import { render, screen } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Button } from './Button'
expect.extend(toHaveNoViolations)
describe('Button Accessibility', () => {
it('should have no accessibility violations', async () => {
const { container } = render(
<Button variant="primary">Click me</Button>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('should have proper ARIA attributes', () => {
render(
<Button disabled aria-label="Save document">
Save
</Button>
)
const button = screen.getByRole('button', { name: 'Save document' })
expect(button).toHaveAttribute('aria-disabled', 'true')
})
})
```
```typescript
// config/env.ts
import { z } from 'zod'
const envSchema = z.object({
// Public variables
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
// Server-only variables
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
// API Keys
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
// Feature flags
ENABLE_ANALYTICS: z.enum(['true', 'false']).transform(v => v === 'true'),
MAINTENANCE_MODE: z.enum(['true', 'false']).transform(v => v === 'true'),
})
// Validate at build time
const parsed = envSchema.safeParse(process.env)
if (!parsed.success) {
console.error('❌ Invalid environment variables:')
console.error(parsed.error.flatten().fieldErrors)
throw new Error('Invalid environment variables')
}
export const env = parsed.data
// Type-safe usage
// import { env } from '@/config/env'
// const apiUrl = env.NEXT_PUBLIC_API_URL
```
```typescript
// config/features.ts
import { env } from './env'
export const features = {
// Static feature flags
newCheckout: true,
socialLogin: false,
// Dynamic feature flags
analytics: env.ENABLE_ANALYTICS,
maintenance: env.MAINTENANCE_MODE,
// User-based flags
betaFeatures: (userId: string) => {
const betaUsers = ['user1', 'user2']
return betaUsers.includes(userId)
},
// Percentage rollout
experimentalSearch: (userId: string) => {
const hash = userId.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0)
}, 0)
return hash % 100 < 20 // 20% of users
},
}
// Feature flag hook
export function useFeature(feature: keyof typeof features) {
const userId = useUserId() // Get from auth context
const value = features[feature]
if (typeof value === 'function') {
return value(userId)
}
return value
}
```
```typescript
// components/Feature.tsx
interface FeatureProps {
flag: keyof typeof features
fallback?: React.ReactNode
children: React.ReactNode
}
export function Feature({ flag, fallback = null, children }: FeatureProps) {
const enabled = useFeature(flag)
if (!enabled) return <>{fallback}</>
return <>{children}</>
}
// Usage
<Feature flag="newCheckout" fallback={<OldCheckout />}>
<NewCheckout />
</Feature>
```
```javascript
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation
'style', // Code style
'refactor', // Code refactoring
'perf', // Performance
'test', // Tests
'chore', // Maintenance
'revert', // Revert commit
'ci', // CI/CD
],
],
'subject-case': [2, 'never', ['upper-case', 'pascal-case']],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [2, 'always', 100],
},
}
```
```json
// package.json
{
"scripts": {
"prepare": "husky install"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
```
```bash
feat: add user authentication with NextAuth
fix: resolve cart total calculation error
docs: update API documentation for v2
perf: optimize image loading with lazy loading
test: add unit tests for checkout flow
Add feature
FEAT: Add login
feat: implemented user authentication system with NextAuth.js and added Google OAuth
```
```
type/scope-description
type/JIRA-123-description
```
- `feature/` - New features
- `fix/` - Bug fixes
- `hotfix/` - Urgent production fixes
- `refactor/` - Code refactoring
- `docs/` - Documentation updates
- `test/` - Test additions/updates
- `chore/` - Maintenance tasks
- `perf/` - Performance improvements
```bash
feature/user-authentication
fix/cart-calculation-error
hotfix/payment-gateway-timeout
refactor/component-structure
docs/api-v2-updates
test/checkout-flow
chore/update-dependencies
perf/image-optimization
```
```typescript
// Use for pages that can be pre-rendered
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await getPosts() // Fetched at build time
return <BlogList posts={posts} />
}
// With revalidation
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
})
return res.json()
}
```
```typescript
// Use for personalized or frequently changing content
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
export default async function DashboardPage() {
const cookieStore = cookies()
const token = cookieStore.get('token')
// This makes the page dynamic
const userData = await getUserData(token)
return <Dashboard data={userData} />
}
```
```typescript
// Static shell with dynamic content
export default async function ProductPage({ params }) {
// Static product data
const product = await getProduct(params.id)
return (
<>
<ProductInfo product={product} />
<Suspense fallback={<ReviewsSkeleton />}>
{/* Dynamic reviews loaded on demand */}
<ProductReviews productId={params.id} />
</Suspense>
</>
)
}
```
```typescript
// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
)
}
```
```typescript
// hooks/usePerformance.ts
export function usePerformance() {
useEffect(() => {
// First Input Delay
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime)
}
}).observe({ entryTypes: ['first-input'] })
// Cumulative Layout Shift
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('CLS:', entry.value)
}
}).observe({ entryTypes: ['layout-shift'] })
// Largest Contentful Paint
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.startTime)
}
}).observe({ entryTypes: ['largest-contentful-paint'] })
}, [])
}
```
```typescript
// Fetch with caching
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600, // Cache for 1 hour
tags: ['products'] // Cache tags for invalidation
}
})
return res.json()
}
// On-demand revalidation
import { revalidateTag, revalidatePath } from 'next/cache'
export async function createProduct(data) {
await db.product.create({ data })
// Invalidate cache
revalidateTag('products')
revalidatePath('/products')
}
```
```typescript
// app/products/page.tsx
export const dynamic = 'force-static' // or 'force-dynamic'
export const revalidate = 3600 // seconds
export const fetchCache = 'force-cache' // or 'force-no-store'
```
```typescript
// React Query setup
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
},
},
})
// Usage in components
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 30 * 60 * 1000, // 30 minutes
})
}
```
```javascript
// .eslintrc.js
module.exports = {
extends: [
'next/core-web-vitals',
'plugin:@typescript-eslint/recommended',
'plugin:tailwindcss/recommended',
'prettier',
],
plugins: ['@typescript-eslint', 'tailwindcss'],
rules: {
// Next.js specific
'@next/next/no-html-link-for-pages': 'error',
'@next/next/no-img-element': 'error',
// TypeScript
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/explicit-module-boundary-types': 'off',
// React
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Import sorting
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
alphabetize: { order: 'asc' },
}],
// Tailwind
'tailwindcss/classnames-order': 'warn',
'tailwindcss/no-custom-classname': 'warn',
},
}
```
```javascript
// jest.config.js
const nextJest = require('next/jest')
const createJestConfig = nextJest({
dir: './',
})
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
}
module.exports = createJestConfig(customJestConfig)
```
```typescript
// components/ui/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('handles click events', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('applies variant styles', () => {
render(<Button variant="danger">Delete</Button>)
const button = screen.getByRole('button')
expect(button).toHaveClass('bg-red-500')
})
})
```
```typescript
// hooks/useDebounce.test.ts
import { renderHook, act } from '@testing-library/react'
import { useDebounce } from './useDebounce'
describe('useDebounce', () => {
jest.useFakeTimers()
it('debounces value changes', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'initial', delay: 500 } }
)
expect(result.current).toBe('initial')
// Update value
rerender({ value: 'updated', delay: 500 })
expect(result.current).toBe('initial')
// Fast-forward time
act(() => {
jest.advanceTimersByTime(500)
})
expect(result.current).toBe('updated')
})
})
```
```typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
})
```
### E2E Test Example
```typescript
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Authentication', () => {
test('user can sign up', async ({ page }) => {
await page.goto('/signup')
// Fill form
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'Password123!')
await page.fill('[name="confirmPassword"]', 'Password123!')
// Submit
await page.click('button[type="submit"]')
// Verify redirect
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('Welcome!')).toBeVisible()
})
test('user can log in', async ({ page }) => {
await page.goto('/login')
await page.fill('[name="email"]', 'existing@example.com')
await page.fill('[name="password"]', 'Password123!')
await Promise.all([
page.waitForNavigation(),
page.click('button[type="submit"]'),
])
await expect(page).toHaveURL('/dashboard')
})
})
```
```typescript
// tests/e2e/api.spec.ts
test('API health check', async ({ request }) => {
const response = await request.get('/api/health')
expect(response.ok()).toBeTruthy()
const data = await response.json()
expect(data).toEqual({
status: 'healthy',
timestamp: expect.any(String),
})
})
```
```javascript
// next.config.js
module.exports = {
experimental: {
webpackBuildWorker: true,
},
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.performance = {
hints: 'error',
maxAssetSize: 250 * 1024, // 250KB
maxEntrypointSize: 250 * 1024,
}
}
return config
},
}
```
```json
// package.json
{
"scripts": {
"analyze": "ANALYZE=true next build",
"analyze:server": "BUNDLE_ANALYZE=server next build",
"analyze:browser": "BUNDLE_ANALYZE=browser next build"
}
}
```
```javascript
// scripts/check-bundle-size.js
const { readFileSync } = require('fs')
const { join } = require('path')
const BUILD_MANIFEST = join(process.cwd(), '.next/build-manifest.json')
const manifest = JSON.parse(readFileSync(BUILD_MANIFEST, 'utf8'))
const BUDGETS = {
'pages/index': 100 * 1024, // 100KB
'pages/products': 150 * 1024, // 150KB
}
Object.entries(BUDGETS).forEach(([page, budget]) => {
const assets = manifest.pages[page]
const totalSize = assets.reduce((sum, asset) => {
const stats = statSync(join('.next', asset))
return sum + stats.size
}, 0)
if (totalSize > budget) {
console.error(`❌ ${page} exceeds budget: ${totalSize} > ${budget}`)
process.exit(1)
}
})
```
```typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Security headers
response.headers.set('X-DNS-Prefetch-Control', 'on')
response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-Frame-Options', 'SAMEORIGIN')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// Content Security Policy
const csp = [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://vercel.live",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com wss://localhost:* ws://localhost:*",
"media-src 'self'",
"frame-ancestors 'none'",
].join('; ')
response.headers.set('Content-Security-Policy', csp)
return response
}
```
```typescript
// middleware/rateLimit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
})
export async function rateLimit(request: NextRequest) {
const ip = request.ip ?? '127.0.0.1'
const { success } = await ratelimit.limit(ip)
if (!success) {
return new Response('Too Many Requests', { status: 429 })
}
}
```
```typescript
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
// Send to error tracking service
captureException(error)
}, [error])
return (
<div className="flex min-h-screen flex-col items-center justify-center">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<button
onClick={reset}
className="mt-4 rounded bg-blue-500 px-4 py-2 text-white"
>
Try again
</button>
</div>
)
}
```
```typescript
// app/loading.tsx
export default function Loading() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-32 w-32 animate-spin rounded-full border-b-2 border-gray-900" />
</div>
)
}
```
This comprehensive guide should help Claude Code work effectively with Next.js projects!