775 lines
16 KiB
Markdown
775 lines
16 KiB
Markdown
---
|
|
name: nextjs-coding-standards
|
|
description: Next.js 16 coding standards including file naming conventions, API patterns, theming, styling guidelines, and directory structure. Use when writing or reviewing code.
|
|
allowed-tools: Read, Grep, Glob
|
|
---
|
|
|
|
# Next.js 16 Coding Standards
|
|
|
|
Reference this skill when writing or reviewing code to ensure consistency with project conventions.
|
|
|
|
---
|
|
|
|
## File Naming Conventions
|
|
|
|
### Files and Directories
|
|
|
|
**Use kebab-case for all file names:**
|
|
|
|
```
|
|
✅ user-profile.tsx
|
|
✅ blog-post-card.tsx
|
|
✅ theme-toggle.tsx
|
|
|
|
❌ UserProfile.tsx
|
|
❌ blogPostCard.tsx
|
|
❌ ThemeToggle.tsx
|
|
```
|
|
|
|
**Why kebab-case?**
|
|
|
|
- Cross-platform compatibility (Windows vs Unix)
|
|
- URL-friendly (file names often map to routes)
|
|
- Easier to parse and read
|
|
- Industry standard for Next.js projects
|
|
|
|
**Special Next.js Files:**
|
|
|
|
```
|
|
page.tsx # Route pages
|
|
layout.tsx # Layout components
|
|
not-found.tsx # 404 pages
|
|
loading.tsx # Loading states
|
|
error.tsx # Error boundaries
|
|
route.ts # API route handlers
|
|
```
|
|
|
|
### Component Names (Inside Files)
|
|
|
|
**Use PascalCase for component names:**
|
|
|
|
```typescript
|
|
// File: user-profile.tsx
|
|
export function UserProfile() {
|
|
return <div>...</div>
|
|
}
|
|
|
|
// File: blog-post-card.tsx
|
|
export default function BlogPostCard() {
|
|
return <article>...</article>
|
|
}
|
|
```
|
|
|
|
### Variables, Functions, Props
|
|
|
|
**Use camelCase:**
|
|
|
|
```typescript
|
|
// Variables
|
|
const userSettings = {}
|
|
const isLoading = false
|
|
|
|
// Functions
|
|
function handleSubmit() {}
|
|
function formatDate(date: string) {}
|
|
|
|
// Props
|
|
interface ButtonProps {
|
|
onClick: () => void
|
|
isDisabled: boolean
|
|
className?: string
|
|
}
|
|
|
|
// Custom Hooks
|
|
function useTheme() {}
|
|
function useMarkdown() {}
|
|
```
|
|
|
|
### Constants
|
|
|
|
**Use SCREAMING_SNAKE_CASE:**
|
|
|
|
```typescript
|
|
const API_BASE_URL = 'https://api.example.com'
|
|
const MAX_RETRIES = 3
|
|
const DEFAULT_LOCALE = 'ro-RO'
|
|
```
|
|
|
|
---
|
|
|
|
## Next.js 16 API Patterns
|
|
|
|
### Route Handlers
|
|
|
|
**Use Route Handlers (not legacy API Routes):**
|
|
|
|
```typescript
|
|
// app/api/posts/route.ts
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
// Export named functions for HTTP methods
|
|
export async function GET(request: NextRequest) {
|
|
const posts = await getAllPosts()
|
|
return NextResponse.json({ posts })
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const body = await request.json()
|
|
// Process request
|
|
return NextResponse.json({ success: true }, { status: 201 })
|
|
}
|
|
```
|
|
|
|
### Type-Safe Validation with Zod
|
|
|
|
**Always validate input with Zod:**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
const bodySchema = z.object({
|
|
title: z.string().min(1).max(200),
|
|
content: z.string(),
|
|
tags: z.array(z.string()).max(3),
|
|
})
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const json = await request.json()
|
|
const parsed = bodySchema.safeParse(json)
|
|
|
|
if (!parsed.success) {
|
|
return NextResponse.json({ error: 'Validation failed', details: parsed.error }, { status: 400 })
|
|
}
|
|
|
|
// parsed.data is fully typed
|
|
const { title, content, tags } = parsed.data
|
|
// ... business logic
|
|
}
|
|
```
|
|
|
|
**Key Points:**
|
|
|
|
- Use `safeParse()` instead of `parse()` to avoid try/catch
|
|
- Return structured error responses
|
|
- Use appropriate HTTP status codes
|
|
- Infer TypeScript types from Zod schemas
|
|
|
|
### Error Handling
|
|
|
|
**Return meaningful status codes:**
|
|
|
|
```typescript
|
|
200 // Success
|
|
201 // Created
|
|
400 // Bad Request (validation errors)
|
|
401 // Unauthorized
|
|
403 // Forbidden
|
|
404 // Not Found
|
|
500 // Internal Server Error
|
|
```
|
|
|
|
**Structured error responses:**
|
|
|
|
```typescript
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Resource not found',
|
|
code: 'NOT_FOUND',
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
{ status: 404 }
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Theming Patterns
|
|
|
|
### next-themes Setup
|
|
|
|
**Root Layout (Server Component):**
|
|
|
|
```typescript
|
|
// app/layout.tsx
|
|
import { ThemeProvider } from 'next-themes'
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html lang="ro" suppressHydrationWarning>
|
|
<body>
|
|
<ThemeProvider
|
|
attribute="class"
|
|
defaultTheme="dark"
|
|
enableSystem={false}
|
|
storageKey="blog-theme"
|
|
>
|
|
{children}
|
|
</ThemeProvider>
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Critical:** Always add `suppressHydrationWarning` to `<html>` tag.
|
|
|
|
### Theme Toggle Component
|
|
|
|
**Avoid hydration mismatches with mounted state:**
|
|
|
|
```typescript
|
|
// components/theme-toggle.tsx
|
|
'use client'
|
|
|
|
import { useTheme } from 'next-themes'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
export function ThemeToggle() {
|
|
const { theme, setTheme } = useTheme()
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
// Prevent hydration mismatch
|
|
useEffect(() => setMounted(true), [])
|
|
|
|
if (!mounted) {
|
|
return <div className="w-9 h-9 animate-pulse" />
|
|
}
|
|
|
|
return (
|
|
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
|
{theme === 'dark' ? '🌙' : '☀️'}
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Pattern:** Always check `mounted` state before rendering theme-dependent UI.
|
|
|
|
### CSS Variables for Theme Tokens
|
|
|
|
**Define in globals.css:**
|
|
|
|
```css
|
|
@layer base {
|
|
:root {
|
|
--bg-primary: 255 255 255;
|
|
--bg-secondary: 248 250 252;
|
|
--text-primary: 15 23 42;
|
|
--text-secondary: 51 65 85;
|
|
}
|
|
|
|
.dark {
|
|
--bg-primary: 24 24 27;
|
|
--bg-secondary: 15 23 42;
|
|
--text-primary: 241 245 249;
|
|
--text-secondary: 203 213 225;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use in components:**
|
|
|
|
```tsx
|
|
<div className="bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))]">
|
|
Theme-aware component
|
|
</div>
|
|
```
|
|
|
|
### Tailwind Configuration
|
|
|
|
**Enable class-based dark mode:**
|
|
|
|
```javascript
|
|
// tailwind.config.js
|
|
module.exports = {
|
|
darkMode: 'class', // Required for next-themes
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
'dark-primary': '#18181b',
|
|
accent: {
|
|
DEFAULT: '#164e63',
|
|
hover: '#155e75',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
**Use dark: variant:**
|
|
|
|
```tsx
|
|
<div className="bg-slate-100 dark:bg-zinc-900 text-slate-900 dark:text-slate-100">
|
|
Automatically switches based on theme
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## Styling Guidelines (Tailwind CSS)
|
|
|
|
### Utility-First Philosophy
|
|
|
|
**Prefer inline utilities over custom CSS:**
|
|
|
|
```tsx
|
|
// ✅ Good: Inline utilities
|
|
<article className="border-4 border-slate-800 p-6 bg-zinc-900 hover:border-cyan-900">
|
|
<h2 className="text-2xl font-bold uppercase tracking-wider">Title</h2>
|
|
</article>
|
|
|
|
// ❌ Avoid: @apply (increases bundle size)
|
|
/* styles.css */
|
|
.card {
|
|
@apply border-4 border-slate-800 p-6 bg-zinc-900;
|
|
}
|
|
```
|
|
|
|
**Exception:** Only use `@apply` for truly global base styles in `globals.css`.
|
|
|
|
### Component Extraction
|
|
|
|
**Extract to React components when reused:**
|
|
|
|
```typescript
|
|
// ✅ Extract repeated patterns to components
|
|
export function Card({ children, className = "" }) {
|
|
return (
|
|
<div className={`border-4 border-slate-800 p-6 bg-zinc-900 ${className}`}>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Usage
|
|
<Card className="hover:border-cyan-900">
|
|
<h2>Title</h2>
|
|
</Card>
|
|
```
|
|
|
|
**Don't extract:** One-off components or single-use patterns.
|
|
|
|
### Class Organization
|
|
|
|
**Group utilities logically:**
|
|
|
|
```tsx
|
|
// Layout → Spacing → Colors → Typography → Effects
|
|
<div className="
|
|
flex flex-col // Layout
|
|
gap-4 p-6 // Spacing
|
|
bg-zinc-900 // Colors
|
|
border-4 border-slate-800
|
|
text-slate-100
|
|
font-mono text-sm // Typography
|
|
uppercase tracking-wider
|
|
hover:border-cyan-900 // Effects
|
|
transition-colors
|
|
">
|
|
```
|
|
|
|
**Tip:** Use Prettier plugin for automatic Tailwind class sorting.
|
|
|
|
### Responsive Design
|
|
|
|
**Mobile-first approach:**
|
|
|
|
```tsx
|
|
// Base classes = mobile, add breakpoints for larger screens
|
|
<div className="
|
|
flex-col // Mobile: vertical stack
|
|
md:flex-row // Tablet+: horizontal layout
|
|
lg:gap-8 // Desktop: more spacing
|
|
">
|
|
```
|
|
|
|
**Standard breakpoints:**
|
|
|
|
```
|
|
sm: 640px // Small tablets
|
|
md: 768px // Tablets
|
|
lg: 1024px // Laptops
|
|
xl: 1280px // Desktops
|
|
2xl: 1536px // Large screens
|
|
```
|
|
|
|
### Conditional Styling
|
|
|
|
**For complex conditions, use variants:**
|
|
|
|
```typescript
|
|
const cardVariants = {
|
|
default: "border-slate-800 bg-zinc-900",
|
|
highlighted: "border-cyan-600 bg-cyan-950",
|
|
error: "border-red-600 bg-red-950",
|
|
}
|
|
|
|
<Card className={cardVariants[variant]} />
|
|
```
|
|
|
|
---
|
|
|
|
## Directory Structure Standards
|
|
|
|
### Project Organization
|
|
|
|
```
|
|
app/
|
|
├── (auth)/ # Route groups (no URL segment)
|
|
│ ├── login/
|
|
│ └── register/
|
|
├── api/ # API routes
|
|
│ └── posts/
|
|
│ └── route.ts
|
|
├── blog/
|
|
│ ├── page.tsx
|
|
│ └── [slug]/
|
|
│ └── page.tsx
|
|
├── @breadcrumbs/ # Parallel routes
|
|
│ └── default.tsx
|
|
├── layout.tsx # Root layout
|
|
├── globals.css
|
|
└── page.tsx
|
|
|
|
components/
|
|
├── blog/ # Domain-specific components
|
|
│ ├── post-card.tsx
|
|
│ └── markdown-renderer.tsx
|
|
├── layout/ # Layout components
|
|
│ ├── header.tsx
|
|
│ └── footer.tsx
|
|
└── ui/ # Reusable UI primitives
|
|
├── button.tsx
|
|
└── card.tsx
|
|
|
|
lib/
|
|
├── api/ # API clients, fetch wrappers
|
|
│ └── client.ts
|
|
├── types/ # TypeScript type definitions
|
|
│ └── post.ts
|
|
├── markdown.ts # Business logic modules
|
|
├── seo.ts
|
|
└── utils.ts # Pure utility functions
|
|
|
|
public/
|
|
├── blog/ # Blog-specific assets
|
|
│ └── images/
|
|
└── icons/
|
|
|
|
content/ # Content files (outside app/)
|
|
└── blog/
|
|
└── posts.md
|
|
```
|
|
|
|
### lib/ Organization
|
|
|
|
**Modules in lib/:**
|
|
|
|
- Substantial business logic (markdown.ts, seo.ts)
|
|
- API clients and data fetching
|
|
- Database connections
|
|
- Authentication logic
|
|
|
|
**Utils in lib/utils.ts:**
|
|
|
|
- Pure helper functions
|
|
- Formatters (formatDate, formatCurrency)
|
|
- Validators (isEmail, isValidUrl)
|
|
- String manipulations
|
|
|
|
**Types in lib/types/:**
|
|
|
|
- Shared TypeScript interfaces
|
|
- API response types
|
|
- Domain models
|
|
- Colocated with their modules when possible
|
|
|
|
### Component Organization
|
|
|
|
**By domain/feature:**
|
|
|
|
```
|
|
components/
|
|
├── blog/ # Blog-specific
|
|
│ ├── post-card.tsx
|
|
│ ├── post-list.tsx
|
|
│ └── markdown-renderer.tsx
|
|
├── auth/ # Auth-specific
|
|
│ ├── login-form.tsx
|
|
│ └── signup-form.tsx
|
|
└── ui/ # Reusable primitives
|
|
├── button.tsx
|
|
├── card.tsx
|
|
└── input.tsx
|
|
```
|
|
|
|
**Not by type:**
|
|
|
|
```
|
|
❌ Don't organize like this:
|
|
components/
|
|
├── forms/
|
|
├── buttons/
|
|
├── cards/
|
|
└── modals/
|
|
```
|
|
|
|
### Public Assets
|
|
|
|
**Organize by feature:**
|
|
|
|
```
|
|
public/
|
|
├── blog/
|
|
│ ├── images/
|
|
│ └── thumbnails/
|
|
├── icons/
|
|
│ ├── social/
|
|
│ └── ui/
|
|
└── fonts/
|
|
```
|
|
|
|
**Naming conventions:**
|
|
|
|
- Use descriptive names: `hero-background.jpg` not `img1.jpg`
|
|
- Use kebab-case: `user-avatar.png`
|
|
- Include dimensions for images: `logo-512x512.png`
|
|
|
|
---
|
|
|
|
## TypeScript Best Practices
|
|
|
|
### Type Safety
|
|
|
|
**Avoid `any`:**
|
|
|
|
```typescript
|
|
// ❌ Bad
|
|
function processData(data: any) {}
|
|
|
|
// ✅ Good
|
|
function processData(data: unknown) {
|
|
if (typeof data === 'string') {
|
|
// TypeScript knows data is string here
|
|
}
|
|
}
|
|
|
|
// ✅ Better: Use specific types
|
|
interface PostData {
|
|
title: string
|
|
content: string
|
|
}
|
|
function processData(data: PostData) {}
|
|
```
|
|
|
|
### Infer Types from Zod
|
|
|
|
**Don't duplicate types:**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
// Define schema once
|
|
const postSchema = z.object({
|
|
title: z.string(),
|
|
content: z.string(),
|
|
tags: z.array(z.string()),
|
|
})
|
|
|
|
// Infer TypeScript type
|
|
type Post = z.infer<typeof postSchema>
|
|
|
|
// Now you have both runtime validation and compile-time types
|
|
```
|
|
|
|
### Type Imports
|
|
|
|
**Use type imports for types only:**
|
|
|
|
```typescript
|
|
// ✅ Good: Explicit type import
|
|
import type { Metadata } from 'next'
|
|
import type { Post } from '@/lib/types/post'
|
|
|
|
// ✅ Mixed: Regular and type imports
|
|
import { getAllPosts } from '@/lib/markdown'
|
|
import type { Post } from '@/lib/types/post'
|
|
```
|
|
|
|
---
|
|
|
|
## Next.js 16 Specific Patterns
|
|
|
|
### Async Server Components
|
|
|
|
**Fetch data directly in components:**
|
|
|
|
```typescript
|
|
// app/blog/page.tsx
|
|
export default async function BlogPage() {
|
|
// Server-side data fetching (no useEffect needed)
|
|
const posts = await getAllPosts()
|
|
|
|
return (
|
|
<div>
|
|
{posts.map(post => (
|
|
<PostCard key={post.slug} post={post} />
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Static Generation
|
|
|
|
**Use generateStaticParams for dynamic routes:**
|
|
|
|
```typescript
|
|
// app/blog/[slug]/page.tsx
|
|
export async function generateStaticParams() {
|
|
const posts = await getAllPosts()
|
|
return posts.map(post => ({
|
|
slug: post.slug.split('/'), // For catch-all routes
|
|
}))
|
|
}
|
|
|
|
export async function generateMetadata({ params }) {
|
|
const post = getPostBySlug(params.slug.join('/'))
|
|
return {
|
|
title: post.frontmatter.title,
|
|
description: post.frontmatter.description,
|
|
}
|
|
}
|
|
|
|
export default async function PostPage({ params }) {
|
|
const post = getPostBySlug(params.slug.join('/'))
|
|
return <article>{/* render post */}</article>
|
|
}
|
|
```
|
|
|
|
### Client Components
|
|
|
|
**Minimize 'use client' usage:**
|
|
|
|
```typescript
|
|
// ❌ Unnecessary client component
|
|
'use client'
|
|
export function StaticCard({ title }) {
|
|
return <div>{title}</div>
|
|
}
|
|
|
|
// ✅ Keep as server component (default)
|
|
export function StaticCard({ title }) {
|
|
return <div>{title}</div>
|
|
}
|
|
|
|
// ✅ Only use 'use client' when necessary
|
|
'use client'
|
|
import { useState } from 'react'
|
|
|
|
export function InteractiveCard({ title }) {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
return (
|
|
<div onClick={() => setIsOpen(!isOpen)}>
|
|
{title}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**When to use 'use client':**
|
|
|
|
- Using React hooks (useState, useEffect, etc.)
|
|
- Using event handlers (onClick, onChange, etc.)
|
|
- Using browser APIs (window, localStorage, etc.)
|
|
- Using context consumers
|
|
|
|
### Parallel Routes
|
|
|
|
**Use for layout composition:**
|
|
|
|
```typescript
|
|
// app/layout.tsx
|
|
export default function RootLayout({
|
|
children,
|
|
breadcrumbs, // From @breadcrumbs parallel route
|
|
}: {
|
|
children: React.ReactNode
|
|
breadcrumbs: React.ReactNode
|
|
}) {
|
|
return (
|
|
<>
|
|
{breadcrumbs}
|
|
<main>{children}</main>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
### 1. Hydration Mismatches
|
|
|
|
**Problem:** Theme-dependent content renders differently on server vs client.
|
|
|
|
**Solution:** Use mounted state pattern (see Theming section).
|
|
|
|
### 2. Image Paths
|
|
|
|
**Problem:** Incorrect public asset paths.
|
|
|
|
```typescript
|
|
// ❌ Wrong
|
|
<Image src="blog/image.jpg" />
|
|
|
|
// ✅ Correct
|
|
<Image src="/blog/image.jpg" /> // Leading slash
|
|
```
|
|
|
|
### 3. Dynamic Route Params
|
|
|
|
**Problem:** Forgetting slug should be an array for catch-all routes.
|
|
|
|
```typescript
|
|
// app/blog/[...slug]/page.tsx
|
|
export async function generateStaticParams() {
|
|
// ❌ Wrong
|
|
return posts.map(post => ({ slug: post.slug }))
|
|
|
|
// ✅ Correct
|
|
return posts.map(post => ({ slug: post.slug.split('/') }))
|
|
}
|
|
```
|
|
|
|
### 4. Over-using Client Components
|
|
|
|
**Problem:** Adding 'use client' unnecessarily.
|
|
|
|
**Solution:** Keep components as Server Components by default. Only add 'use client' when you need hooks, events, or browser APIs.
|
|
|
|
### 5. Date Formats
|
|
|
|
**Problem:** Inconsistent date formatting.
|
|
|
|
**Solution:** Use consistent ISO format (YYYY-MM-DD) in data, format for display:
|
|
|
|
```typescript
|
|
// In frontmatter
|
|
date: '2025-01-15'
|
|
|
|
// For display
|
|
formatDate(post.frontmatter.date) // "15 ianuarie 2025" (Romanian)
|
|
```
|
|
|
|
---
|
|
|
|
## Reference
|
|
|
|
For project-specific architecture and design philosophy, see `CLAUDE.md` at the root of this repository.
|
|
|
|
This skill focuses on coding conventions and standards. For architecture patterns, markdown processing, and industrial design aesthetic guidelines, refer to the main documentation.
|