- 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>
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-themesfor 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 viaPromise.all, renders Hero directly, wraps below-fold sections inScrollFadeInapp/components/Hero.tsx— server component (no"use client"), usesgetTranslations()fromnext-intl/serverapp/components/ClientWrappers.tsx— exportsAboutClient,ProjectsClient,ContactClient,FooterClient; each wraps its component in a scopedNextIntlClientProviderwith only the needed translation namespaceapp/components/ClientProviders.tsx— root client wrapper, defers Three.js/WebGL viarequestIdleCallback(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:
- Directus CMS (if
DIRECTUS_STATIC_TOKENconfigured) → 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, usesdirectusRequest()with 2s timeout - Returns
nullon failure, never throws - Collections:
tech_stack_categories,tech_stack_items,hobbies,content_pages,projects,book_reviews - Translations use Directus native M2O system; locale mapping:
en→en-US,de→de-DE - API routes must export
runtime = 'nodejs',dynamic = 'force-dynamic', and include asourcefield in the response ("directus"|"fallback"|"error")
n8n Integration
- Webhook proxies in
app/api/n8n/(status, chat, hardcover, generate-image) - Auth via
N8N_SECRET_TOKENand/orN8N_API_KEYheaders - 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-pinkliquid-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 fromlib/directus.tsortypes/ - 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.jsonandmessages/de.json;useTranslations()in client,getTranslations()in server components - Error logging:
console.erroronly whenprocess.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.tsmockswindow.matchMedia,IntersectionObserver, andNextResponse - ESM modules (react-markdown, remark-*, etc.) handled via
transformIgnorePatternsinjest.config.ts - Server component tests:
const resolved = await Component({ props }); render(resolved) - Test mocks for
next/image: useeslint-disable-next-line @next/next/no-img-elementon the<img>tag
Deployment & CI/CD
output: "standalone"innext.config.ts- Entrypoint:
scripts/start-with-migrate.js— waits for DB, runs migrations (non-fatal), starts server - CI/CD:
.gitea/workflows/ci.yml—test-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
- Define GraphQL query + types in
lib/directus.ts - Create API route
app/api/<name>/route.tswithruntime='nodejs',dynamic='force-dynamic', andsourcefield in response - Create component
app/components/<Name>.tsxwith Skeleton loading state - Add i18n keys to both
messages/en.jsonandmessages/de.json - Create
<Name>Clientwrapper inapp/components/ClientWrappers.tsxwith scopedNextIntlClientProvider - Add to
app/_ui/HomePageServer.tsxwrapped in<ScrollFadeIn>