Files
portfolio/CLAUDE.md
denshooter 7f9d39c275
All checks were successful
CI / CD / test-build (push) Successful in 10m59s
CI / CD / deploy-dev (push) Successful in 1m54s
CI / CD / deploy-production (push) Has been skipped
perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents
- Replace ShaderGradientBackground WebGL shader (3 static spheres) with pure
  CSS radial-gradient divs — moves from ClientProviders (deferred JS) to
  app/layout.tsx as a server component rendered in initial HTML. Eliminates
  @shadergradient/react, three, @react-three/fiber from the JS bundle.
  Removes chunks/7001 (~20s CPU eval) and the 39s main thread block.

- Remove optimizeCss/critters: it was converting <link rel="stylesheet"> to a
  JS-deferred preload, which PageSpeed read as a 410ms sequential CSS chain.
  Both CSS files now load as parallel <link> tags from initial HTML (~150ms).

- Update browserslist safari >= 15 → 15.4 (Array.prototype.at, Object.hasOwn
  are native in 15.4+; eliminates unnecessary SWC compatibility transforms).

- Delete orphaned app/styles/ghostContent.css (never imported anywhere, 3.7KB).

- Add .claude/ dev team setup: 5 subagents (frontend-dev, backend-dev, tester,
  code-reviewer, debugger), 3 skills (/add-section, /review-changes,
  /check-quality), 3 path-scoped rules, settings.json with auto-lint hook.

- Update CLAUDE.md with server/client orchestrator pattern, SSR animation
  safety rules, API route conventions, and improved command reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:40:01 +01:00

6.7 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, and Framer Motion. Uses a "Liquid Editorial Bento" design system with soft gradient colors and glassmorphism effects.

Tech Stack

  • Framework: Next.js 15 (App Router), TypeScript 5.9, React 19
  • Styling: Tailwind CSS 3.4 with custom liquid-* color tokens
  • Theming: next-themes for Dark Mode support (system/light/dark)
  • Animations: Framer Motion 12
  • 3D: Three.js + React Three Fiber + @shadergradient/react (shader gradient background)
  • Database: PostgreSQL via Prisma ORM
  • Cache: Redis (optional)
  • CMS: Directus (self-hosted, GraphQL only, optional)
  • Automation: n8n webhooks (status, chat, hardcover, image generation)
  • i18n: next-intl (EN + DE), message files in messages/
  • Deployment: Docker + Nginx, CI via Gitea Actions (output: "standalone")

Commands

npm run dev          # Full dev environment (Docker + Next.js)
npm run dev:simple   # Next.js only (no Docker)
npm run dev:next     # Plain Next.js dev server
npm run build        # Production build
npm run lint         # ESLint (0 errors required, warnings OK)
npm run lint:fix     # Auto-fix lint issues
npm run test         # All Jest unit tests
npx jest path/to/test.tsx  # Run a single test file
npm run test:watch   # Watch mode
npm run test:e2e     # Playwright E2E tests
npm run db:generate  # Regenerate Prisma client after schema changes

Architecture

Server/Client Component Split

The homepage uses a server component orchestrator pattern:

  • app/_ui/HomePageServer.tsx — async server component, fetches all translations in parallel via Promise.all, renders Hero directly, wraps below-fold sections in ScrollFadeIn
  • app/components/Hero.tsxserver component (no "use client"), uses getTranslations() from next-intl/server
  • app/components/ClientWrappers.tsx — exports AboutClient, ProjectsClient, ContactClient, FooterClient; each wraps its component in a scoped NextIntlClientProvider with only the needed translation namespace
  • app/components/ClientProviders.tsx — root client wrapper, defers Three.js/WebGL via requestIdleCallback (5s timeout) to avoid blocking LCP

SSR Animation Safety

Never use Framer Motion's initial={{ opacity: 0 }} on SSR-rendered elements — it bakes style="opacity:0" into HTML, making content invisible if JS hydration fails or is slow.

Use ScrollFadeIn (app/components/ScrollFadeIn.tsx) instead: renders no inline style during SSR, applies opacity+transform only after hasMounted check via IntersectionObserver + CSS transitions.

AnimatePresence is fine for modals/overlays that only render after user interaction.

Data Source Fallback Chain

Every data fetch degrades gracefully — the site never crashes:

  1. Directus CMS (if DIRECTUS_STATIC_TOKEN configured) → 2. PostgreSQL → 3. JSON files (messages/*.json) → 4. Hardcoded defaults → 5. i18n key itself

CMS Integration (Directus)

  • GraphQL via lib/directus.ts — no Directus SDK, uses directusRequest() with 2s timeout
  • Returns null on failure, never throws
  • Collections: tech_stack_categories, tech_stack_items, hobbies, content_pages, projects, book_reviews
  • Translations use Directus native M2O system; locale mapping: enen-US, dede-DE
  • API routes must export runtime = 'nodejs', dynamic = 'force-dynamic', and include a source field in the response ("directus" | "fallback" | "error")

n8n Integration

  • Webhook proxies in app/api/n8n/ (status, chat, hardcover, generate-image)
  • Auth via N8N_SECRET_TOKEN and/or N8N_API_KEY headers
  • All endpoints have rate limiting and 10s timeout
  • Hardcover reading data cached 5 minutes

Design System

Custom Tailwind colors prefixed with liquid-:

  • liquid-sky, liquid-mint, liquid-lavender, liquid-pink
  • liquid-rose, liquid-peach, liquid-coral, liquid-teal, liquid-lime

Cards: bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 with backdrop-blur-sm, border-2, rounded-xl.

Typography: Headlines uppercase, tracking-tighter, accent dot at end (<span className="text-emerald-600">.</span>).

Accessibility: Use text-stone-600 dark:text-stone-400 (not text-stone-400 alone) for body text — contrast ratio must be ≥4.5:1.

Conventions

  • TypeScript: No any — use interfaces from lib/directus.ts or types/
  • Components: PascalCase files in app/components/; every async component needs a Skeleton loading state
  • API routes: kebab-case directories in app/api/
  • i18n: Always add keys to both messages/en.json and messages/de.json; useTranslations() in client, getTranslations() in server components
  • Error logging: console.error only when process.env.NODE_ENV === "development"
  • Commit messages: Conventional Commits (feat:, fix:, chore:)
  • No emojis in code unless explicitly requested

Testing Notes

  • Jest with JSDOM; jest.setup.ts mocks window.matchMedia, IntersectionObserver, and NextResponse
  • ESM modules (react-markdown, remark-*, etc.) handled via transformIgnorePatterns in jest.config.ts
  • Server component tests: const resolved = await Component({ props }); render(resolved)
  • Test mocks for next/image: use eslint-disable-next-line @next/next/no-img-element on the <img> tag

Deployment & CI/CD

  • output: "standalone" in next.config.ts
  • Entrypoint: scripts/start-with-migrate.js — waits for DB, runs migrations (non-fatal), starts server
  • CI/CD: .gitea/workflows/ci.ymltest-build (all branches), deploy-dev (dev branch only), deploy-production (production branch only)
  • Branches: dev → testing.dk0.dev | production → dk0.dev
  • Dev and production share the same PostgreSQL and Redis instances

Key Environment Variables

DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=...
N8N_WEBHOOK_URL=https://n8n.dk0.dev
N8N_SECRET_TOKEN=...
N8N_API_KEY=...
DATABASE_URL=postgresql://...
REDIS_URL=redis://...  # optional

Adding a CMS-managed Section

  1. Define GraphQL query + types in lib/directus.ts
  2. Create API route app/api/<name>/route.ts with runtime='nodejs', dynamic='force-dynamic', and source field in response
  3. Create component app/components/<Name>.tsx with Skeleton loading state
  4. Add i18n keys to both messages/en.json and messages/de.json
  5. Create <Name>Client wrapper in app/components/ClientWrappers.tsx with scoped NextIntlClientProvider
  6. Add to app/_ui/HomePageServer.tsx wrapped in <ScrollFadeIn>