Files
mypage/.claude/skills/nextjs-coding-standards/SKILL.md
2025-11-14 15:33:00 +02:00

16 KiB

name, description, allowed-tools
name description allowed-tools
nextjs-coding-standards Next.js 16 coding standards including file naming conventions, API patterns, theming, styling guidelines, and directory structure. Use when writing or reviewing code. 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:

// 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:

// 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:

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):

// 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:

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:

200 // Success
201 // Created
400 // Bad Request (validation errors)
401 // Unauthorized
403 // Forbidden
404 // Not Found
500 // Internal Server Error

Structured error responses:

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):

// 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:

// 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:

@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:

<div className="bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))]">
  Theme-aware component
</div>

Tailwind Configuration

Enable class-based dark mode:

// 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:

<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:

// ✅ 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:

// ✅ 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:

// 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:

// 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:

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:

// ❌ 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:

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:

// ✅ 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:

// 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:

// 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:

// ❌ 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:

// 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.

// ❌ 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.

// 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:

// 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.