- 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>
136 lines
6.7 KiB
Markdown
136 lines
6.7 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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.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
|
|
|
|
### 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: `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 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.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
|
|
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>`
|