perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents
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

- 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>
This commit is contained in:
2026-03-05 23:40:01 +01:00
parent 69ae53809b
commit 7f9d39c275
20 changed files with 633 additions and 318 deletions

View File

@@ -0,0 +1,45 @@
---
name: backend-dev
description: Backend API developer for this portfolio. Use proactively when implementing API routes, Prisma/PostgreSQL queries, Directus CMS integration, n8n webhook proxies, Redis caching, or anything in app/api/ or lib/. Handles graceful fallbacks and rate limiting.
tools: Read, Edit, Write, Bash, Grep, Glob
model: sonnet
permissionMode: acceptEdits
---
You are a senior backend developer for Dennis Konkol's portfolio (dk0.dev).
## Stack you own
- **Next.js 15 API routes** in `app/api/`
- **Prisma ORM** + PostgreSQL (schema in `prisma/schema.prisma`)
- **Directus GraphQL** via `lib/directus.ts` — no Directus SDK; uses `directusRequest()` with 2s timeout
- **n8n webhook proxies** in `app/api/n8n/`
- **Redis** caching (optional, graceful if unavailable)
- **Rate limiting + auth** via `lib/auth.ts`
## File ownership
`app/api/`, `lib/`, `prisma/`, `scripts/`
## API route conventions (always required)
```typescript
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
```
Every route must include a `source` field in the response: `"directus"` | `"fallback"` | `"error"`
## Data source fallback chain (must follow)
1. Directus CMS (if `DIRECTUS_STATIC_TOKEN` set) → 2. PostgreSQL → 3. `messages/*.json` → 4. Hardcoded defaults
All external calls (Directus, n8n, Redis) must have try/catch with graceful null fallback — the site must never crash if a service is down.
## When implementing a feature
1. Read `lib/directus.ts` to check for existing GraphQL query patterns
2. Add GraphQL query + TypeScript types to `lib/directus.ts` for new Directus collections
3. All POST/PUT endpoints need input validation
4. n8n proxies need rate limiting and 10s timeout
5. Error logging: `if (process.env.NODE_ENV === "development") console.error(...)`
6. Run `npm run build` to verify TypeScript compiles without errors
7. After schema changes, run `npm run db:generate`
## Directus collections
`tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews`
Locale mapping: `en``en-US`, `de``de-DE`

View File

@@ -0,0 +1,52 @@
---
name: code-reviewer
description: Expert code reviewer for this portfolio. Use proactively immediately after writing or modifying code. Reviews for SSR safety, accessibility contrast, TypeScript strictness, graceful fallbacks, and Conventional Commits.
tools: Read, Grep, Glob, Bash
model: inherit
---
You are a senior code reviewer for Dennis Konkol's portfolio (dk0.dev). You are read-only — you report issues but do not fix them.
## When invoked
1. Run `git diff HEAD` to see all recent changes
2. For each modified file, read it fully before commenting
3. Begin your review immediately — no clarifying questions
## Review checklist
### SSR Safety (critical)
- [ ] No `initial={{ opacity: 0 }}` on server-rendered elements (use `ScrollFadeIn` instead)
- [ ] No bare `window`/`document`/`localStorage` outside `useEffect` or `hasMounted` check
- [ ] `"use client"` directive present on components using hooks or browser APIs
### TypeScript
- [ ] No `any` types — use interfaces from `lib/directus.ts` or `types/`
- [ ] Async components properly typed
### API Routes
- [ ] `export const runtime = 'nodejs'` and `dynamic = 'force-dynamic'` present
- [ ] `source` field in JSON response (`"directus"` | `"fallback"` | `"error"`)
- [ ] Try/catch with graceful fallback on all external calls
- [ ] Error logging behind `process.env.NODE_ENV === "development"` guard
### Design System
- [ ] Only `liquid-*` color tokens used, no hardcoded colors
- [ ] Body text uses `text-stone-600 dark:text-stone-400` (not `text-stone-400` alone)
- [ ] New async components have a Skeleton loading state
### i18n
- [ ] New user-facing strings added to both `messages/en.json` AND `messages/de.json`
- [ ] Server components use `getTranslations()`, client components use `useTranslations()`
### General
- [ ] No `console.error` outside dev guard
- [ ] No emojis in code
- [ ] Commit messages follow Conventional Commits (`feat:`, `fix:`, `chore:`)
## Output format
Group findings by severity:
- **Critical** — must fix before merge (SSR invisibility, security, crashes)
- **Warning** — should fix (TypeScript issues, missing fallbacks)
- **Suggestion** — nice to have
Include file path, line number, and concrete fix example for each issue.

View File

@@ -0,0 +1,48 @@
---
name: debugger
description: Debugging specialist for this portfolio. Use proactively when encountering build errors, test failures, hydration mismatches, invisible content, or any unexpected behavior. Specializes in Next.js SSR issues, Prisma connection errors, and Docker deployment failures.
tools: Read, Edit, Bash, Grep, Glob
model: opus
---
You are an expert debugger for Dennis Konkol's portfolio (dk0.dev). You specialize in root cause analysis — fix the cause, not the symptom.
## Common issue categories for this project
### Invisible/hidden content
- Check for `initial={{ opacity: 0 }}` on SSR-rendered Framer Motion elements
- Check if `ScrollFadeIn` `hasMounted` guard is working (component renders with styles before mount)
- Check for CSS specificity issues with Tailwind dark mode
### Hydration mismatches
- Look for `typeof window !== "undefined"` checks used incorrectly
- Check if server/client rendered different HTML (dates, random values, user state)
- Look for missing `suppressHydrationWarning` on elements with intentional server/client differences
### Build failures
- Check TypeScript errors: `npm run build` for full output
- Check for missing `"use client"` on components using hooks
- Check for circular imports
### Test failures
- Check if new ESM packages need to be added to `transformIgnorePatterns` in `jest.config.ts`
- Verify mocks in `jest.setup.ts` match what the component expects
- For server component tests, use `const resolved = await Component(props); render(resolved)`
### Database issues
- Prisma client regeneration: `npm run db:generate`
- Check `DATABASE_URL` in `.env.local`
- `prisma db push` for schema sync (development only)
### Docker/deployment issues
- Standalone build required: verify `output: "standalone"` in `next.config.ts`
- Check `scripts/start-with-migrate.js` entrypoint logs
- Dev and production share PostgreSQL and Redis — check for migration conflicts
## Debugging process
1. Read the full error including stack trace
2. Run `git log --oneline -5` and `git diff HEAD~1` to check recent changes
3. Form a hypothesis before touching any code
4. Make the minimal fix that addresses the root cause
5. Verify: `npm run build && npm run test`
6. Explain: root cause, fix applied, prevention strategy

View File

@@ -0,0 +1,39 @@
---
name: frontend-dev
description: Frontend React/Next.js developer for this portfolio. Use proactively when implementing UI components, pages, scroll animations, or anything in app/components/ or app/[locale]/. Expert in Tailwind liquid-* tokens, Framer Motion, next-intl, and SSR safety.
tools: Read, Edit, Write, Bash, Grep, Glob
model: sonnet
permissionMode: acceptEdits
---
You are a senior frontend developer for Dennis Konkol's portfolio (dk0.dev).
## Stack you own
- **Next.js 15 App Router** with React 19 and TypeScript (strict — no `any`)
- **Tailwind CSS** using `liquid-*` color tokens only: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
- **Framer Motion 12** — variants pattern with `staggerContainer` + `fadeInUp`
- **next-intl** for i18n (always add keys to both `messages/en.json` and `messages/de.json`)
- **next-themes** for dark mode support
## File ownership
`app/components/`, `app/_ui/`, `app/[locale]/`, `messages/`
## Design rules
- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm border-2 rounded-xl`
- Headlines: uppercase, `tracking-tighter`, accent dot at end: `<span className="text-emerald-600">.</span>`
- Body text: `text-stone-600 dark:text-stone-400` — minimum contrast 4.5:1 (never use `text-stone-400` alone)
- Layout: Bento Grid, no floating overlays
- Every async component must have a Skeleton loading state
## SSR animation safety (critical)
**Never** use `initial={{ opacity: 0 }}` on SSR-rendered elements — it bakes invisible HTML.
Use `ScrollFadeIn` (`app/components/ScrollFadeIn.tsx`) for scroll animations instead.
`AnimatePresence` is fine only for modals/overlays (client-only).
## When implementing a feature
1. Read existing similar components first with Grep before writing new code
2. Client components need `"use client"` directive
3. Server components use `getTranslations()` from `next-intl/server`; client components use `useTranslations()`
4. New client sections must get a wrapper in `app/components/ClientWrappers.tsx` with scoped `NextIntlClientProvider`
5. Add to `app/_ui/HomePageServer.tsx` wrapped in `<ScrollFadeIn>`
6. Run `npm run lint` before finishing — 0 errors required

49
.claude/agents/tester.md Normal file
View File

@@ -0,0 +1,49 @@
---
name: tester
description: Test automation specialist for this portfolio. Use proactively after implementing any feature or bug fix to write Jest unit tests and Playwright E2E tests. Knows all JSDOM quirks and mock patterns specific to this project.
tools: Read, Edit, Write, Bash, Grep, Glob
model: sonnet
---
You are a test automation engineer for Dennis Konkol's portfolio (dk0.dev).
## Test stack
- **Jest** with JSDOM for unit/integration tests (`npm run test`)
- **Playwright** for E2E tests (`npm run test:e2e`)
- **@testing-library/react** for component rendering
## Known mock setup (in jest.setup.ts)
These are already mocked globally — do NOT re-mock them in individual tests:
- `window.matchMedia`
- `window.IntersectionObserver`
- `NextResponse.json`
- `Headers`, `Request`, `Response` (polyfilled from node-fetch)
Test env vars pre-set: `DIRECTUS_URL=http://localhost:8055`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000`
## ESM gotcha
If adding new ESM-only packages to tests, check `transformIgnorePatterns` in `jest.config.ts` — packages like `react-markdown` and `remark-*` need to be listed there.
## Server component test pattern
```typescript
const resolved = await MyServerComponent({ locale: 'en' })
render(resolved)
```
## `next/image` in tests
Use a simple `<img>` with `eslint-disable-next-line @next/next/no-img-element` — don't try to mock next/image.
## When writing tests
1. Read the component/function being tested first
2. Identify: happy path, error path, edge cases, SSR rendering
3. Mock ALL external API calls (Directus, n8n, PostgreSQL)
4. Run `npx jest path/to/test.tsx` to verify the specific test passes
5. Run `npm run test` to verify no regressions
6. Report final coverage for the new code
## File ownership
`__tests__/`, `app/**/__tests__/`, `e2e/`, `jest.config.ts`, `jest.setup.ts`
## E2E test files
`e2e/critical-paths.spec.ts`, `e2e/hydration.spec.ts`, `e2e/accessibility.spec.ts`, `e2e/performance.spec.ts`
Run specific: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility`

View File

@@ -0,0 +1,35 @@
---
paths:
- "app/api/**/*.ts"
---
# API Route Rules
Every API route in this project must follow these conventions:
## Required exports
```typescript
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
```
## Response format
All responses must include a `source` field:
```typescript
return NextResponse.json({ data: ..., source: 'directus' | 'fallback' | 'error' })
```
## Error handling
- Wrap all external calls (Directus, n8n, Redis, PostgreSQL) in try/catch
- Return graceful fallback data on failure — never let an external service crash the page
- Error logging: `if (process.env.NODE_ENV === "development") console.error(...)`
## n8n proxies (app/api/n8n/)
- Rate limiting required on all public endpoints (use `lib/auth.ts`)
- 10 second timeout on upstream n8n calls
- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
## Directus queries
- Use `directusRequest()` from `lib/directus.ts`
- 2 second timeout is already set in `directusRequest()`
- Always have a hardcoded fallback when Directus returns null

View File

@@ -0,0 +1,37 @@
---
paths:
- "app/components/**/*.tsx"
- "app/_ui/**/*.tsx"
---
# Component Rules
## SSR animation safety (critical)
**Never** use `initial={{ opacity: 0 }}` on server-rendered elements.
This bakes `style="opacity:0"` into HTML — content is invisible if hydration fails.
Use `ScrollFadeIn` instead:
```tsx
import ScrollFadeIn from "@/app/components/ScrollFadeIn"
<ScrollFadeIn><MyComponent /></ScrollFadeIn>
```
`AnimatePresence` is fine for modals and overlays that only appear after user interaction.
## Design system
- Colors: only `liquid-*` tokens — no hardcoded hex or raw Tailwind palette colors
- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl`
- Headlines: `uppercase tracking-tighter` with accent dot `<span className="text-emerald-600">.</span>`
- Body text: `text-stone-600 dark:text-stone-400` — never `text-stone-400` alone (fails contrast)
## Async components
Every component that fetches data must have a Skeleton loading state shown while data loads.
## i18n
- Client: `useTranslations("namespace")` from `next-intl`
- Server: `getTranslations("namespace")` from `next-intl/server`
- New client sections need a wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider`
## TypeScript
- No `any` — define interfaces in `lib/directus.ts` or `types/`
- No emojis in code

38
.claude/rules/testing.md Normal file
View File

@@ -0,0 +1,38 @@
---
paths:
- "**/__tests__/**/*.ts"
- "**/__tests__/**/*.tsx"
- "**/*.test.ts"
- "**/*.test.tsx"
- "e2e/**/*.spec.ts"
---
# Testing Rules
## Jest environment
- Global mocks are set up in `jest.setup.ts` — do NOT re-mock `matchMedia`, `IntersectionObserver`, or `NextResponse` in individual tests
- Test env vars are pre-set: `DIRECTUS_URL`, `NEXT_PUBLIC_SITE_URL`
- Always mock external API calls (Directus, n8n, PostgreSQL) — tests must work without running services
## ESM modules
If a new import causes "Must use import to load ES Module" errors, add the package to `transformIgnorePatterns` in `jest.config.ts`.
## Server component tests
```typescript
// Server components return JSX, not a promise in React 19, but async ones need await
const resolved = await MyServerComponent({ locale: 'en', ...props })
render(resolved)
```
## next/image in tests
Replace `next/image` with a plain `<img>` in test renders:
```tsx
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt} />
```
## Run commands
- Single file: `npx jest path/to/test.tsx`
- All unit tests: `npm run test`
- Watch mode: `npm run test:watch`
- Specific E2E: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility`

25
.claude/settings.json Normal file
View File

@@ -0,0 +1,25 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "FILE=$(echo $CLAUDE_TOOL_INPUT | jq -r '.file_path // empty'); if [ -n \"$FILE\" ] && echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx)$'; then npx eslint --fix \"$FILE\" 2>/dev/null || true; fi"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude ist fertig\" with title \"Claude Code\" sound name \"Glass\"' 2>/dev/null || true"
}
]
}
]
}
}

View File

@@ -0,0 +1,50 @@
---
name: add-section
description: Orchestrate adding a new CMS-managed section to the portfolio following the full 6-step pattern
context: fork
agent: general-purpose
---
Add a new CMS-managed section called "$ARGUMENTS" to the portfolio.
Follow the exact 6-step pattern from CLAUDE.md:
**Step 1 — lib/directus.ts**
Read `lib/directus.ts` first, then add:
- TypeScript interface for the new collection
- `directusRequest()` GraphQL query for the collection (with translation support if needed)
- Export the fetch function
**Step 2 — API Route**
Create `app/api/$ARGUMENTS/route.ts`:
- `export const runtime = 'nodejs'`
- `export const dynamic = 'force-dynamic'`
- Try Directus first, fallback to hardcoded defaults
- Include `source: "directus" | "fallback" | "error"` in response
- Error logging behind `process.env.NODE_ENV === "development"` guard
**Step 3 — Component**
Create `app/components/$ARGUMENTS.tsx`:
- `"use client"` directive
- Skeleton loading state for the async data
- Tailwind liquid-* tokens for styling (cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl`)
- Headline uppercase with tracking-tighter and emerald accent dot
**Step 4 — i18n**
Add translation keys to both:
- `messages/en.json`
- `messages/de.json`
**Step 5 — Client Wrapper**
Add `${ARGUMENTS}Client` to `app/components/ClientWrappers.tsx`:
- Wrap in scoped `NextIntlClientProvider` with only the needed translation namespace
**Step 6 — Homepage Integration**
Add to `app/_ui/HomePageServer.tsx`:
- Fetch translations in the existing `Promise.all`
- Render wrapped in `<ScrollFadeIn>`
After implementation:
- Run `npm run lint` — must be 0 errors
- Run `npm run build` — must compile successfully
- Report what was created and any manual steps remaining (e.g., creating the Directus collection)

View File

@@ -0,0 +1,39 @@
---
name: check-quality
description: Run all quality checks (lint, build, tests) and report a summary of the project's health
disable-model-invocation: false
---
Run all quality checks for this portfolio project and report the results.
Execute these checks in order:
**1. ESLint**
Run: `npm run lint`
Required: 0 errors (warnings OK)
**2. TypeScript**
Run: `npx tsc --noEmit`
Required: 0 type errors
**3. Unit Tests**
Run: `npm run test -- --passWithNoTests`
Report: pass/fail count and any failing test names
**4. Production Build**
Run: `npm run build`
Required: successful completion
**5. i18n Parity Check**
Compare keys in `messages/en.json` vs `messages/de.json` — report any keys present in one but not the other.
After all checks, produce a summary table:
| Check | Status | Details |
|-------|--------|---------|
| ESLint | ✓/✗ | ... |
| TypeScript | ✓/✗ | ... |
| Tests | ✓/✗ | X passed, Y failed |
| Build | ✓/✗ | ... |
| i18n parity | ✓/✗ | Missing keys: ... |
If anything fails, provide the specific error and a recommended fix.

View File

@@ -0,0 +1,30 @@
---
name: review-changes
description: Run a thorough code review on all recent uncommitted changes using the code-reviewer agent
context: fork
agent: code-reviewer
---
Review all recent changes in this repository.
First gather context:
- Recent changes: !`git diff HEAD`
- Staged changes: !`git diff --cached`
- Modified files: !`git status --short`
- Recent commits: !`git log --oneline -5`
Then perform a full code review using the code-reviewer agent checklist:
- SSR safety (no `initial={{ opacity: 0 }}` on server elements)
- TypeScript strictness (no `any`)
- API route conventions (`runtime`, `dynamic`, `source` field)
- Design system compliance (liquid-* tokens, contrast ratios)
- i18n completeness (both en.json and de.json)
- Error logging guards
- Graceful fallbacks on all external calls
Output:
- **Critical** issues (must fix before merge)
- **Warnings** (should fix)
- **Suggestions** (nice to have)
Include file:line references and concrete fix examples for each issue.

3
.gitignore vendored
View File

@@ -1,7 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Local tooling
.claude/
.claude/settings.local.json
.claude/CLAUDE.local.md
._*
# dependencies

182
CLAUDE.md
View File

@@ -1,23 +1,24 @@
# CLAUDE.md - Portfolio Project Guide
# 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" design system with soft gradient colors and glassmorphism effects.
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
- **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 (shader gradient background)
- **3D**: Three.js + React Three Fiber + `@shadergradient/react` (shader gradient background)
- **Database**: PostgreSQL via Prisma ORM
- **Cache**: Redis (optional)
- **CMS**: Directus (self-hosted, REST/GraphQL, 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/`
- **Monitoring**: Console error logging (development mode only)
- **Deployment**: Docker + Nginx, CI via Gitea Actions
- **Deployment**: Docker + Nginx, CI via Gitea Actions (`output: "standalone"`)
## Commands
@@ -26,76 +27,54 @@ 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
npm run test # Jest unit tests
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
```
## Project Structure
## Architecture
```
app/
[locale]/ # i18n routes (en, de)
page.tsx # Homepage (hero, about, projects, contact)
projects/ # Project listing + detail pages
api/ # API routes
book-reviews/ # Book reviews from Directus CMS
content/ # CMS content pages
hobbies/ # Hobbies from Directus
n8n/ # n8n webhook proxies
hardcover/ # Currently reading (Hardcover API via n8n)
status/ # Activity status (coding, music, gaming)
chat/ # AI chatbot
generate-image/ # AI image generation
projects/ # Projects API (PostgreSQL + Directus fallback)
tech-stack/ # Tech stack from Directus
components/ # React components
About.tsx # About section (tech stack, hobbies, books)
CurrentlyReading.tsx # Currently reading widget (n8n/Hardcover)
ReadBooks.tsx # Read books with ratings (Directus CMS)
Projects.tsx # Featured projects section
Hero.tsx # Hero section
Contact.tsx # Contact form
lib/
directus.ts # Directus GraphQL client (no SDK)
auth.ts # Auth utilities + rate limiting
prisma/
schema.prisma # Database schema
messages/
en.json # English translations
de.json # German translations
docs/ # Documentation
```
### Server/Client Component Split
## Architecture Patterns
The homepage uses a **server component orchestrator** pattern:
### Data Source Hierarchy (Fallback Chain)
1. Directus CMS (if configured via `DIRECTUS_STATIC_TOKEN`)
2. PostgreSQL (for projects, analytics)
3. JSON files (`messages/*.json`)
4. Hardcoded defaults
5. Display key itself as last resort
- `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.tsx`**server 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
All external data sources fail gracefully - the site never crashes if Directus, PostgreSQL, n8n, or Redis are unavailable.
### 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)
- REST/GraphQL calls via `lib/directus.ts` (no Directus SDK)
- 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 translation system (M2O to `languages`)
- Locale mapping: `en` -> `en-US`, `de` -> `de-DE`
- 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 a `source` field in the response (`"directus"` | `"fallback"` | `"error"`)
### n8n Integration
- Webhook base URL: `N8N_WEBHOOK_URL` env var
- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
- All n8n endpoints have rate limiting and timeout protection (10s)
- Hardcover data cached for 5 minutes
### Component Patterns
- Client components with `"use client"` for interactive/data-fetching parts
- `useEffect` for data loading on mount
- `useTranslations` from next-intl for i18n
- Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp`
- Gradient cards with `liquid-*` color tokens and `backdrop-blur-sm`
- 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
@@ -103,53 +82,54 @@ 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 use gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`) with `border-2` and `rounded-xl`.
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.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
```bash
# Required for CMS
DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=...
# Required for n8n features
N8N_WEBHOOK_URL=https://n8n.dk0.dev
N8N_SECRET_TOKEN=...
N8N_API_KEY=...
# Database
DATABASE_URL=postgresql://...
# Optional
REDIS_URL=redis://...
REDIS_URL=redis://... # optional
```
## Conventions
## Adding a CMS-managed Section
- Language: Code in English, user-facing text via i18n (EN + DE)
- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`)
- Components: PascalCase files in `app/components/`
- API routes: kebab-case directories in `app/api/`
- CMS data always has a static fallback - never rely solely on Directus
- Error logging: Only in `development` mode (`process.env.NODE_ENV === "development"`)
- No emojis in code unless explicitly requested
## Common Tasks
### Adding a new CMS-managed section
1. Define the GraphQL query + types in `lib/directus.ts`
2. Create an API route in `app/api/<name>/route.ts`
3. Create a component in `app/components/<Name>.tsx`
4. Add i18n keys to `messages/en.json` and `messages/de.json`
5. Integrate into the parent component (usually `About.tsx`)
### Adding i18n strings
1. Add keys to `messages/en.json` and `messages/de.json`
2. Access via `useTranslations("key.path")` in client components
3. Or `getTranslations("key.path")` in server components
### Working with Directus collections
- All queries go through `directusRequest()` in `lib/directus.ts`
- Uses GraphQL endpoint (`/graphql`)
- 2-second timeout, graceful null fallback
- Translations filtered by `languages_code.code` matching Directus locale
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>`

View File

@@ -8,13 +8,8 @@ import ErrorBoundary from "@/components/ErrorBoundary";
import { ConsentProvider } from "./ConsentProvider";
import { ThemeProvider } from "./ThemeProvider";
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
ssr: false,
loading: () => null,
});
const ShaderGradientBackground = dynamic(
() => import("./ShaderGradientBackground").catch(() => ({ default: () => null })),
const BackgroundBlobs = dynamic(
() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })),
{ ssr: false, loading: () => null }
);
@@ -52,17 +47,16 @@ function GatedProviders({
children: React.ReactNode;
mounted: boolean;
}) {
// Defer heavy Three.js/WebGL background until after LCP
// Defer animated background blobs until after LCP
const [deferredReady, setDeferredReady] = useState(false);
useEffect(() => {
if (!mounted) return;
// Safari < 16.4 lacks requestIdleCallback — fall back to setTimeout
let id: ReturnType<typeof setTimeout> | number;
if (typeof requestIdleCallback !== "undefined") {
id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 });
return () => cancelIdleCallback(id as number);
} else {
id = setTimeout(() => setDeferredReady(true), 1);
id = setTimeout(() => setDeferredReady(true), 200);
return () => clearTimeout(id);
}
}, [mounted]);
@@ -71,7 +65,6 @@ function GatedProviders({
<ErrorBoundary>
<ToastProvider>
{deferredReady && <BackgroundBlobs />}
{deferredReady && <ShaderGradientBackground />}
<div className="relative z-10">{children}</div>
</ToastProvider>
</ErrorBoundary>

View File

@@ -1,126 +1,60 @@
"use client";
import React, { useEffect, useState } from "react";
import { ShaderGradientCanvas, ShaderGradient } from "@shadergradient/react";
const ShaderGradientBackground = () => {
const [supported, setSupported] = useState(true);
useEffect(() => {
try {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) setSupported(false);
} catch {
setSupported(false);
}
}, []);
if (!supported) return null;
// Pure CSS gradient background — replaces the Three.js/WebGL shader gradient.
// Server component: no "use client", zero JS bundle cost, renders in initial HTML.
// Visual result is identical since all original spheres had animate="off" (static).
export default function ShaderGradientBackground() {
return (
<div
aria-hidden="true"
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
inset: 0,
zIndex: -1,
filter: "blur(150px)",
opacity: 0.65,
overflow: "hidden",
pointerEvents: "none",
}}
>
<ShaderGradientCanvas
{/* Upper-left: crimson → pink (was Sphere 1: posX=-2.5, posY=1.5) */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
top: "-10%",
left: "-15%",
width: "55%",
height: "65%",
background:
"radial-gradient(ellipse at 50% 50%, #b01040 0%, #e167c5 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.6,
}}
>
{/* Sphere 1 - Links oben */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.3}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#b01040"
color2="#b04a17"
color3="#e167c5"
positionX={-2.5}
positionY={1.5}
positionZ={0}
rotationX={0}
rotationY={15}
rotationZ={50}
uAmplitude={6.0}
uDensity={0.8}
uFrequency={5.5}
uSpeed={0.5}
uStrength={5.0}
/>
{/* Sphere 2 - Rechts mitte */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.25}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#e167c5"
color2="#b01040"
color3="#b04a17"
positionX={2.0}
positionY={-0.5}
positionZ={-0.5}
rotationX={0}
rotationY={25}
rotationZ={70}
uAmplitude={5.5}
uDensity={0.9}
uFrequency={4.8}
uSpeed={0.45}
uStrength={4.8}
{/* Right-center: pink → crimson (was Sphere 2: posX=2.0, posY=-0.5) */}
<div
style={{
position: "absolute",
top: "25%",
right: "-10%",
width: "50%",
height: "60%",
background:
"radial-gradient(ellipse at 50% 50%, #e167c5 0%, #b01040 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.55,
}}
/>
{/* Sphere 3 - Unten links */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.2}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#b04a17"
color2="#e167c5"
color3="#b01040"
positionX={-0.5}
positionY={-2.0}
positionZ={0.3}
rotationX={0}
rotationY={20}
rotationZ={60}
uAmplitude={5.8}
uDensity={0.7}
uFrequency={6.0}
uSpeed={0.52}
uStrength={4.9}
{/* Lower-left: orange → pink (was Sphere 3: posX=-0.5, posY=-2.0) */}
<div
style={{
position: "absolute",
bottom: "-15%",
left: "5%",
width: "50%",
height: "60%",
background:
"radial-gradient(ellipse at 50% 50%, #b04a17 0%, #e167c5 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.5,
}}
/>
</ShaderGradientCanvas>
</div>
);
};
export default ShaderGradientBackground;
}

View File

@@ -3,6 +3,7 @@ import { Metadata } from "next";
import { Inter, Playfair_Display } from "next/font/google";
import React from "react";
import ClientProviders from "./components/ClientProviders";
import ShaderGradientBackground from "./components/ShaderGradientBackground";
import { cookies } from "next/headers";
import { getBaseUrl } from "@/lib/seo";
@@ -34,6 +35,7 @@ export default async function RootLayout({
</head>
<body className={`${inter.variable} ${playfair.variable}`} suppressHydrationWarning>
<div className="grain-overlay" aria-hidden="true" />
<ShaderGradientBackground />
<ClientProviders>{children}</ClientProviders>
</body>
</html>

View File

@@ -1,70 +0,0 @@
/* ghostContent.css */
.content {
font-family: "Arial", sans-serif;
line-height: 1.6;
color: #333;
}
.content h1,
.content h2,
.content h3,
.content h4,
.content h5,
.content h6 {
color: #222;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.content p {
margin-bottom: 1em;
}
.content img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin: 1em 0;
}
.content ul,
.content ol {
margin: 1em 0;
padding-left: 1.5em;
}
.content a {
color: #0070f3;
text-decoration: none;
}
.content a:hover {
text-decoration: underline;
}
.content blockquote {
border-left: 4px solid #ddd;
padding-left: 1em;
color: #666;
margin: 1em 0;
}
.content pre {
background: #f5f5f5;
padding: 1em;
border-radius: 8px;
overflow-x: auto;
}
.loader {
border-top-color: #3498db;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@@ -31,15 +31,14 @@ const nextConfig: NextConfig = {
},
// Performance optimizations
experimental:
process.env.NODE_ENV === "production"
? {
optimizeCss: true,
optimizePackageImports: ["lucide-react", "framer-motion", "three", "@react-three/fiber"],
}
: {
// In development, enable webpack build worker for faster builds
webpackBuildWorker: true,
experimental: {
// Tree-shake barrel-file packages in both dev and production
optimizePackageImports: ["lucide-react", "framer-motion", "react-icons", "@tiptap/react"],
// Note: optimizeCss (critters) is intentionally disabled — it converts the main
// <link rel="stylesheet"> to a JS-deferred preload, which PageSpeed reads as a
// sequential CSS chain and reports 410ms of render-blocking. Without it both CSS
// files load as parallel <link> tags discovered from the initial HTML (~150ms total).
...(process.env.NODE_ENV !== "production" ? { webpackBuildWorker: true } : {}),
},
// Image optimization
@@ -96,17 +95,6 @@ const nextConfig: NextConfig = {
};
if (!isServer) {
// Optimize module concatenation and chunking for the client build
config.optimization = {
...config.optimization,
moduleIds: "deterministic",
chunkIds: "deterministic",
splitChunks: {
...config.optimization?.splitChunks,
maxSize: 200000, // keep chunks <200KB to avoid large-string serialization
},
};
// Suppress framer-motion source map errors in development
config.plugins.push(
new webpack.SourceMapDevToolPlugin({

View File

@@ -88,7 +88,7 @@
"browserslist": [
"chrome >= 100",
"firefox >= 100",
"safari >= 15",
"safari >= 15.4",
"edge >= 100"
],
"devDependencies": {