-> Added claude contexts
This commit is contained in:
758
.claude/skills/nextjs-coding-standards/SKILL.md
Normal file
758
.claude/skills/nextjs-coding-standards/SKILL.md
Normal file
@@ -0,0 +1,758 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user