2 Commits

Author SHA1 Message Date
RJ
bc745cfa8b -> Added claude contexts 2025-11-12 16:17:55 +02:00
RJ
06155c9dbe - fixed routing 2025-11-12 16:17:29 +02:00
7 changed files with 1280 additions and 186 deletions

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

332
CLAUDE.md Normal file
View File

@@ -0,0 +1,332 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **Next.js 16** blog/portfolio application built with **TypeScript**, **Tailwind CSS**, and **React 19**. The project uses **App Router** with Static Site Generation (SSG) for blog posts stored as Markdown files.
**Design Philosophy:** Industrial/SCP-inspired aesthetic with terminal/cyberpunk elements. Sharp edges, thick borders, monospace fonts, darker color palettes (slate/zinc/cyan/emerald tones). No modern Material UI feel - think government documents, classified files, brutal utilitarian design.
## Development Commands
```bash
# Development server (runs on port 3030)
npm run dev
# Production build
npm run build
# Start production server
npm run start
# Lint code
npm run lint
# Validate all markdown posts (frontmatter, format, tags)
npm run validate-posts
```
## Architecture Overview
### Next.js 16 App Router Structure
```
app/
├── @breadcrumbs/ # Parallel route for breadcrumb navigation
│ ├── default.tsx # Auto-generated breadcrumbs
│ ├── blog/[...slug]/ # Post-specific breadcrumbs with titles
│ ├── tags/[tag]/ # Tag-specific breadcrumbs
│ └── about/ # Static breadcrumbs
├── blog/
│ ├── page.tsx # Blog listing with all posts
│ └── [...slug]/ # Dynamic post routes (supports nested paths)
│ ├── page.tsx # Post rendering with SSG
│ └── not-found.tsx # Custom 404 for missing posts
├── about/page.tsx
├── layout.tsx # Root layout with metadata, fonts, breadcrumbs slot
└── page.tsx # Landing page (hero + featured posts)
```
**Key Architectural Patterns:**
1. **Parallel Routes:** `@breadcrumbs` slot renders dynamic navigation based on current route without prop drilling
2. **Catch-all Routes:** `[...slug]` supports nested blog posts (e.g., `/blog/tech/article-name`)
3. **Static Generation:** `generateStaticParams()` pre-renders all blog posts at build time
4. **Server Components by Default:** All components are RSC unless marked with `'use client'`
### Markdown System
```
content/blog/ # Markdown files (supports nested directories)
├── example.md
└── tech/
└── article.md
lib/
├── markdown.ts # Core markdown utilities
│ ├── getPostBySlug() # Read single post with path sanitization
│ ├── getAllPosts() # Get all posts, sorted by date, recursive
│ ├── getRelatedPosts() # Find similar posts by tags
│ └── validateFrontmatter()
├── types/frontmatter.ts # TypeScript interfaces for Post, FrontMatter
└── utils.ts # formatDate(), formatRelativeDate(), generateExcerpt()
```
**Frontmatter Schema:**
```yaml
---
title: string # Required
description: string # Required
date: "YYYY-MM-DD" # Required, ISO format
author: string # Required
tags: [string, string?, string?] # Max 3 tags
image?: string # Optional hero image
draft?: boolean # Exclude from listings if true
---
```
**Security:** Path sanitization prevents directory traversal attacks. All file reads use `path.resolve()` and validate paths stay within `content/blog/`.
### Components Organization
```
components/
├── blog/
│ └── MarkdownRenderer.tsx # Client component for rendering markdown
│ # Custom components: images (Next Image),
│ # links (external vs internal), code blocks
├── layout/
│ ├── Breadcrumbs.tsx # Client component, uses usePathname()
│ └── BreadcrumbsSchema.tsx # Schema.org structured data for SEO
└── [future components]
```
## Coding Standards for Next.js 16
### File Naming Conventions
**Files and Directories:**
- Use **kebab-case** for all file names: `user-profile.tsx`, `blog-post.tsx`
- Special Next.js files: `page.tsx`, `layout.tsx`, `not-found.tsx`, `loading.tsx`
**Component Names (inside files):**
- Use **PascalCase**: `export function UserProfile()`, `export default BlogPost`
**Variables, Functions, Props:**
- Use **camelCase**: `const userSettings = {}`, `function handleSubmit() {}`
- Hooks: `useTheme`, `useMarkdown`
**Constants:**
- Use **SCREAMING_SNAKE_CASE**: `const API_BASE_URL = "..."`
**Why kebab-case for files?**
- Cross-platform compatibility (Windows vs Unix)
- URL-friendly (file names often map to routes)
- Easier to parse and read
### Theme Management & Reusability
**Recommended Pattern:** Use `next-themes` library for dark/light mode
```typescript
// Root layout.tsx
import { ThemeProvider } from 'next-themes'
export default function RootLayout({ children }) {
return (
<html suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
storageKey="blog-theme"
>
{children}
</ThemeProvider>
</body>
</html>
)
}
```
**Client Component for Toggle:**
```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>...</div>
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle
</button>
}
```
**Tailwind Configuration:**
```javascript
// tailwind.config.js
module.exports = {
darkMode: 'class', // Use 'class' strategy for next-themes
theme: {
extend: {
colors: {
// Define custom colors for consistency
'dark-primary': '#18181b',
'accent': { DEFAULT: '#164e63', hover: '#155e75' }
}
}
}
}
```
**CSS Variables Pattern:**
```css
/* globals.css */
:root {
--bg-primary: 255 255 255;
--text-primary: 15 23 42;
}
.dark {
--bg-primary: 24 24 27;
--text-primary: 241 245 249;
}
/* Use in components */
.card {
@apply bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))];
}
```
### Provider Pattern Best Practices
1. **Server Component Boundary:** Keep `layout.tsx` as Server Component, wrap only `children` with Client Provider
2. **Avoid Hydration Mismatches:** Always use `suppressHydrationWarning` on `<html>` tag
3. **Client-Only Rendering:** Use `useEffect` + `mounted` state for theme-dependent UI
4. **Context Consumption:** Only components using `useTheme()` need `'use client'` directive
5. **No Prop Drilling:** Context makes theme accessible anywhere without passing props
### Next.js 16 Specific Patterns
**Async Server Components:**
```typescript
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await getAllPosts() // Server-side data fetching
return <div>{posts.map(...)}</div>
}
```
**Static Generation with Dynamic Routes:**
```typescript
// app/blog/[...slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug.split('/') }))
}
export async function generateMetadata({ params }) {
const post = getPostBySlug(params.slug.join('/'))
return { title: post.frontmatter.title, ... }
}
```
**Parallel Routes 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}
{children}
</>
}
```
## Project-Specific Patterns
### Markdown Processing
- **Always validate paths:** Use `sanitizePath()` to prevent directory traversal
- **Draft support:** Posts with `draft: true` are excluded from `getAllPosts()`
- **Recursive directories:** Blog posts can be organized in subdirectories (`content/blog/tech/post.md`)
- **Reading time:** Auto-calculated at 200 words/minute
- **Date handling:** Use Romanian locale (`ro-RO`) for date formatting
### SEO & Metadata
- **Every page exports `metadata`:** Use Next.js 16's `Metadata` type
- **Dynamic metadata:** Use `generateMetadata()` for blog posts
- **Structured data:** Include Schema.org `BreadcrumbList` and `BlogPosting`
- **OpenGraph images:** Reference `post.frontmatter.image` for social sharing
### Styling Guidelines
**Color Palette:**
- Backgrounds: `zinc-900`, `slate-900`, `slate-800`
- Accents: `cyan-900`, `emerald-900`, `teal-900`
- Text: `slate-100`, `slate-300`, `slate-500`
- Borders: `border-2`, `border-4` (thick, sharp)
**Design Tokens:**
- **NO rounded corners:** Use `rounded-none` or omit (default is sharp)
- **Monospace fonts:** Apply `font-mono` for terminal aesthetic
- **Uppercase labels:** Use `uppercase tracking-wider` for headers
- **Border-heavy design:** Thick borders (`border-4`) over shadows
- **Classification labels:** Add metadata like "FILE#001", "DOCUMENT LEVEL-1"
**Typography:**
- Primary font: `JetBrains Mono` (monospace)
- Headings: `font-mono font-bold uppercase`
- Body: `font-mono text-sm`
- Code blocks: Sharp borders, dark background, no syntax highlighting (for terminal feel)
## Available Subagents
Use these specialized agents via `/spec-implementation-and-review` command:
- `nextjs-specialist` - Next.js 15/16, App Router, SSG, API routes
- `ui-implementer` - UI implementation with shadcn/ui, Tailwind
- `ui-css-specialist` - CSS layouts, styling, responsive design
- `react-frontend-expert` - React components, hooks, state management
- `nodejs-typescript-engineer` - TypeScript, Node.js backend
## Common Pitfalls
1. **Hydration Mismatches with Themes:** Always use `suppressHydrationWarning` on `<html>` and check `mounted` state before rendering theme-dependent UI
2. **Image Paths:** Use `/` prefix for public assets (`/blog/image.jpg` not `blog/image.jpg`)
3. **Dynamic Routes:** Remember to return `slug` as array in `generateStaticParams()` for catch-all routes
4. **Client Components:** Minimize `'use client'` usage - only add when using hooks, event handlers, or browser APIs
5. **Path Security:** Always use `sanitizePath()` when reading markdown files
6. **Date Formats:** Use `YYYY-MM-DD` in frontmatter, convert to Romanian locale for display
7. **Port Configuration:** Dev server runs on port **3030** (not default 3000)
## Type Safety
- All utilities in `lib/` are fully typed
- Frontmatter structure enforced via `FrontMatter` interface
- Use `Post` type for blog post objects
- Avoid `any` - use `unknown` if type is truly unknown, then narrow
## Performance Notes
- **SSG by default:** All blog posts pre-rendered at build time
- **Image optimization:** Use Next.js `<Image>` component
- **Font optimization:** Google Fonts loaded via `next/font`
- **No client-side data fetching:** Markdown loaded server-side only
- **Static exports:** Pages are fully static HTML (no server required for hosting)

180
app/blog/blog-client.tsx Normal file
View File

@@ -0,0 +1,180 @@
'use client'
import { useMemo, useState } from 'react'
import { Post } from '@/lib/types/frontmatter'
import { BlogCard } from '@/components/blog/blog-card'
import { SearchBar } from '@/components/blog/search-bar'
import { SortDropdown } from '@/components/blog/sort-dropdown'
import { TagFilter } from '@/components/blog/tag-filter'
import { Navbar } from '@/components/blog/navbar'
interface BlogPageClientProps {
posts: Post[]
allTags: string[]
}
type SortOption = 'newest' | 'oldest' | 'title'
export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {
const [searchQuery, setSearchQuery] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [sortBy, setSortBy] = useState<SortOption>('newest')
const [currentPage, setCurrentPage] = useState(1)
const postsPerPage = 9
const filteredAndSortedPosts = useMemo(() => {
let result = posts.filter((post) => {
const matchesSearch =
searchQuery === '' ||
post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase())
const matchesTags =
selectedTags.length === 0 ||
selectedTags.every((tag) => post.frontmatter.tags.includes(tag))
return matchesSearch && matchesTags
})
result.sort((a, b) => {
switch (sortBy) {
case 'oldest':
return new Date(a.frontmatter.date).getTime() - new Date(b.frontmatter.date).getTime()
case 'title':
return a.frontmatter.title.localeCompare(b.frontmatter.title)
case 'newest':
default:
return new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
}
})
return result
}, [posts, searchQuery, selectedTags, sortBy])
const totalPages = Math.ceil(filteredAndSortedPosts.length / postsPerPage)
const paginatedPosts = filteredAndSortedPosts.slice(
(currentPage - 1) * postsPerPage,
currentPage * postsPerPage
)
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
setCurrentPage(1)
}
return (
<div className="min-h-screen bg-zinc-900">
<Navbar />
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */}
<div className="border-l-4 border-cyan-400 pl-6 mb-12">
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest mb-2">
DATABASE QUERY // SEARCH RESULTS
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-zinc-100 uppercase tracking-tight">
&gt; BLOG ARCHIVE_
</h1>
</div>
{/* Search Bar */}
<div className="border-4 border-slate-700 bg-slate-900 p-6 mb-8">
<div className="flex flex-col lg:flex-row gap-4">
<SearchBar
searchQuery={searchQuery}
onSearchChange={(value) => {
setSearchQuery(value)
setCurrentPage(1)
}}
/>
<SortDropdown
sortBy={sortBy}
onSortChange={setSortBy}
/>
</div>
</div>
{/* Tag Filters */}
<TagFilter
allTags={allTags}
selectedTags={selectedTags}
onToggleTag={toggleTag}
onClearTags={() => {
setSelectedTags([])
setCurrentPage(1)
}}
/>
{/* Results Count */}
<div className="mb-6">
<p className="font-mono text-sm text-zinc-500 uppercase">
FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}
</p>
</div>
{/* Blog Grid */}
{paginatedPosts.length > 0 ? (
<div className="grid gap-8 lg:grid-cols-3 md:grid-cols-2 grid-cols-1 mb-12">
{paginatedPosts.map((post, index) => {
const hasImage = !!post.frontmatter.image
let variant: 'image-top' | 'image-side' | 'text-only'
if (!hasImage) {
variant = 'text-only'
} else {
variant = index % 3 === 1 ? 'image-side' : 'image-top'
}
return <BlogCard key={post.slug} post={post} variant={variant} />
})}
</div>
) : (
<div className="border-4 border-slate-700 bg-slate-900 p-12 text-center">
<p className="font-mono text-lg text-zinc-400 uppercase">
NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS
</p>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="border-4 border-slate-700 bg-slate-900 p-6">
<div className="flex items-center justify-between">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-6 py-3 font-mono text-sm uppercase border-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 transition-colors cursor-pointer"
>
&lt; PREV
</button>
<div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`w-12 h-12 font-mono text-sm border-2 transition-colors cursor-pointer ${
currentPage === page
? 'bg-cyan-400 border-cyan-400 text-slate-900'
: 'border-slate-700 text-zinc-400 hover:border-cyan-400 hover:text-cyan-400'
}`}
>
{String(page).padStart(2, '0')}
</button>
))}
</div>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-6 py-3 font-mono text-sm uppercase border-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 transition-colors cursor-pointer"
>
NEXT &gt;
</button>
</div>
</div>
)}
</div>
</div>
)
}

View File

@@ -1,15 +1,10 @@
import { Metadata } from 'next'
import { getAllPosts } from '@/lib/markdown'
import BlogPageClient from './page'
export const metadata: Metadata = {
title: 'Blog',
description: 'Toate articolele din blog',
}
export default async function BlogLayout() {
const posts = await getAllPosts()
const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort()
return <BlogPageClient posts={posts} allTags={allTags} />
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View File

@@ -1,180 +1,9 @@
'use client'
import { getAllPosts } from '@/lib/markdown'
import BlogPageClient from './blog-client'
import { useMemo, useState } from 'react'
import { Post } from '@/lib/types/frontmatter'
import { BlogCard } from '@/components/blog/blog-card'
import { SearchBar } from '@/components/blog/search-bar'
import { SortDropdown } from '@/components/blog/sort-dropdown'
import { TagFilter } from '@/components/blog/tag-filter'
import { Navbar } from '@/components/blog/navbar'
export default async function BlogPage() {
const posts = await getAllPosts()
const allTags = Array.from(new Set(posts.flatMap((post) => post.frontmatter.tags))).sort()
interface BlogPageClientProps {
posts: Post[]
allTags: string[]
}
type SortOption = 'newest' | 'oldest' | 'title'
export default function BlogPageClient({ posts, allTags }: BlogPageClientProps) {
const [searchQuery, setSearchQuery] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [sortBy, setSortBy] = useState<SortOption>('newest')
const [currentPage, setCurrentPage] = useState(1)
const postsPerPage = 9
const filteredAndSortedPosts = useMemo(() => {
let result = posts.filter((post) => {
const matchesSearch =
searchQuery === '' ||
post.frontmatter.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.frontmatter.description.toLowerCase().includes(searchQuery.toLowerCase())
const matchesTags =
selectedTags.length === 0 ||
selectedTags.every((tag) => post.frontmatter.tags.includes(tag))
return matchesSearch && matchesTags
})
result.sort((a, b) => {
switch (sortBy) {
case 'oldest':
return new Date(a.frontmatter.date).getTime() - new Date(b.frontmatter.date).getTime()
case 'title':
return a.frontmatter.title.localeCompare(b.frontmatter.title)
case 'newest':
default:
return new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime()
}
})
return result
}, [posts, searchQuery, selectedTags, sortBy])
const totalPages = Math.ceil(filteredAndSortedPosts.length / postsPerPage)
const paginatedPosts = filteredAndSortedPosts.slice(
(currentPage - 1) * postsPerPage,
currentPage * postsPerPage
)
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
)
setCurrentPage(1)
}
return (
<div className="min-h-screen bg-zinc-900">
<Navbar />
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Header */}
<div className="border-l-4 border-cyan-400 pl-6 mb-12">
<p className="font-mono text-xs text-zinc-500 uppercase tracking-widest mb-2">
DATABASE QUERY // SEARCH RESULTS
</p>
<h1 className="text-4xl md:text-6xl font-mono font-bold text-zinc-100 uppercase tracking-tight">
&gt; BLOG ARCHIVE_
</h1>
</div>
{/* Search Bar */}
<div className="border-4 border-slate-700 bg-slate-900 p-6 mb-8">
<div className="flex flex-col lg:flex-row gap-4">
<SearchBar
searchQuery={searchQuery}
onSearchChange={(value) => {
setSearchQuery(value)
setCurrentPage(1)
}}
/>
<SortDropdown
sortBy={sortBy}
onSortChange={setSortBy}
/>
</div>
</div>
{/* Tag Filters */}
<TagFilter
allTags={allTags}
selectedTags={selectedTags}
onToggleTag={toggleTag}
onClearTags={() => {
setSelectedTags([])
setCurrentPage(1)
}}
/>
{/* Results Count */}
<div className="mb-6">
<p className="font-mono text-sm text-zinc-500 uppercase">
FOUND {filteredAndSortedPosts.length} {filteredAndSortedPosts.length === 1 ? 'POST' : 'POSTS'}
</p>
</div>
{/* Blog Grid */}
{paginatedPosts.length > 0 ? (
<div className="grid gap-8 lg:grid-cols-3 md:grid-cols-2 grid-cols-1 mb-12">
{paginatedPosts.map((post, index) => {
const hasImage = !!post.frontmatter.image
let variant: 'image-top' | 'image-side' | 'text-only'
if (!hasImage) {
variant = 'text-only'
} else {
variant = index % 3 === 1 ? 'image-side' : 'image-top'
}
return <BlogCard key={post.slug} post={post} variant={variant} />
})}
</div>
) : (
<div className="border-4 border-slate-700 bg-slate-900 p-12 text-center">
<p className="font-mono text-lg text-zinc-400 uppercase">
NO POSTS FOUND // TRY DIFFERENT SEARCH TERMS
</p>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="border-4 border-slate-700 bg-slate-900 p-6">
<div className="flex items-center justify-between">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-6 py-3 font-mono text-sm uppercase border-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 transition-colors cursor-pointer"
>
&lt; PREV
</button>
<div className="flex items-center gap-2">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`w-12 h-12 font-mono text-sm border-2 transition-colors cursor-pointer ${
currentPage === page
? 'bg-cyan-400 border-cyan-400 text-slate-900'
: 'border-slate-700 text-zinc-400 hover:border-cyan-400 hover:text-cyan-400'
}`}
>
{String(page).padStart(2, '0')}
</button>
))}
</div>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="px-6 py-3 font-mono text-sm uppercase border-2 border-slate-700 text-zinc-100 disabled:opacity-30 disabled:cursor-not-allowed hover:border-cyan-400 hover:text-cyan-400 transition-colors cursor-pointer"
>
NEXT &gt;
</button>
</div>
</div>
)}
</div>
</div>
)
return <BlogPageClient posts={posts} allTags={allTags} />
}

View File

@@ -53,7 +53,7 @@ export function validateFrontmatter(data: any): FrontMatter {
}
export function getPostBySlug(slug: string | string[]): Post | null {
const slugArray = Array.isArray(slug) ? slug : [slug];
const slugArray = Array.isArray(slug) ? slug : slug.split('/');
const sanitized = slugArray.map(s => sanitizePath(s));
const fullPath = path.join(POSTS_PATH, ...sanitized) + '.md';

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.