docs: update copilot instructions with SSR patterns and CI/CD changes
All checks were successful
CI / CD / test-build (push) Successful in 11m3s
CI / CD / deploy-dev (push) Successful in 1m18s
CI / CD / deploy-production (push) Has been skipped

- Document ScrollFadeIn pattern and Framer Motion SSR pitfall
- Update server/client component architecture section
- Reflect combined CI/CD workflow structure
- Add accessibility contrast requirements
- Streamline commands and conventions sections

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-04 23:47:17 +01:00
parent 77db462c22
commit 4a8cb5867f

View File

@@ -1,210 +1,107 @@
# Portfolio Project Instructions # Portfolio Project Instructions
This is Dennis Konkol's personal portfolio (dk0.dev) - a Next.js 15 portfolio with Directus CMS integration, n8n automation, and a "liquid" design system. Dennis Konkol's portfolio (dk0.dev) Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system.
## Build, Test, and Lint ## Build, Test, and Lint
### Development
```bash ```bash
npm run dev # Full dev environment (Docker + Next.js) npm run dev:next # Plain Next.js dev server (no Docker)
npm run dev:simple # Next.js only (no Docker dependencies) npm run build # Production build (standalone mode)
npm run dev:next # Plain Next.js dev server 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
``` ```
### Build & Deploy ## Architecture
```bash
npm run build # Production build (standalone mode)
npm run start # Start production server
```
### Testing ### Server/Client Component Split
```bash
# Unit tests (Jest)
npm run test # Run all unit tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage report
# E2E tests (Playwright) The homepage uses a **server component orchestrator** pattern:
npm run test:e2e # Run all E2E tests
npm run test:e2e:ui # Interactive UI mode
npm run test:critical # Critical paths only
npm run test:hydration # Hydration tests only
```
### Linting - `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps client sections in `ScrollFadeIn`
```bash - `app/components/Hero.tsx`**server component** (no `"use client"`), uses `getTranslations()` from `next-intl/server`
npm run lint # Run ESLint - `app/components/ClientWrappers.tsx` — exports `AboutClient`, `ProjectsClient`, `ContactClient`, `FooterClient`, each wrapping their component in a scoped `NextIntlClientProvider` with only the needed translation keys
npm run lint:fix # Auto-fix issues - `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP
```
### Database (Prisma) ### SSR Animation Safety
```bash
npm run db:generate # Generate Prisma client
npm run db:push # Push schema to database
npm run db:studio # Open Prisma Studio
npm run db:seed # Seed database
```
## Architecture Overview **Never use Framer Motion's `initial={{ opacity: 0 }}` on SSR-rendered elements** — it bakes `style="opacity:0"` into HTML, making content invisible if hydration fails.
### Tech Stack Use `ScrollFadeIn` component instead (`app/components/ScrollFadeIn.tsx`): renders no inline style during SSR (content visible by default), applies opacity+transform only after `hasMounted` check, animates via IntersectionObserver + CSS transitions.
- **Framework**: Next.js 15 (App Router), TypeScript 5.9
- **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens
- **Theming**: next-themes for dark mode (system/light/dark)
- **Animations**: Framer Motion 12
- **3D**: Three.js + React Three Fiber (shader gradient background)
- **Database**: PostgreSQL via Prisma ORM
- **Cache**: Redis (optional)
- **CMS**: Directus (self-hosted, GraphQL, optional)
- **Automation**: n8n webhooks (status, chat, hardcover, image generation)
- **i18n**: next-intl (EN + DE)
- **Monitoring**: Console error logging (development mode only)
- **Deployment**: Docker (standalone mode) + Nginx
### Key Directories Framer Motion `AnimatePresence` is fine for modals/overlays that only render after user interaction.
```
app/
[locale]/ # i18n routes (en, de)
page.tsx # Homepage sections
projects/ # Project listing + detail pages
api/ # API routes
book-reviews/ # Book reviews from Directus
hobbies/ # Hobbies from Directus
n8n/ # n8n webhook proxies
projects/ # Projects (PostgreSQL + Directus)
tech-stack/ # Tech stack from Directus
components/ # React components
lib/
directus.ts # Directus GraphQL client (no SDK)
auth.ts # Auth + rate limiting
translations-loader.ts # i18n loaders for server components
prisma/
schema.prisma # Database schema
messages/
en.json # English translations
de.json # German translations
```
### Data Source Fallback Chain ### Data Source Fallback Chain
The architecture prioritizes resilience with this fallback hierarchy:
1. **Directus CMS** (if `DIRECTUS_STATIC_TOKEN` configured)
2. **PostgreSQL** (for projects, analytics)
3. **JSON files** (`messages/*.json`)
4. **Hardcoded defaults**
5. **Display key itself** (last resort)
**Critical**: The site never crashes if external services (Directus, PostgreSQL, n8n, Redis) are unavailable. All API routes return graceful fallbacks. Every data fetch degrades gracefully — the site never crashes:
1. **Directus CMS** → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself**
### CMS Integration (Directus) ### CMS Integration (Directus)
- GraphQL calls via `lib/directus.ts` (no Directus SDK)
- Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews` - GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout
- Translations use Directus native system (M2O to `languages`) - Returns `null` on failure (never throws)
- Locale mapping: `en``en-US`, `de``de-DE` - Locale mapping: `en``en-US`, `de``de-DE`
- API routes export `runtime='nodejs'`, `dynamic='force-dynamic'` and include a `source` field in JSON responses (`directus|fallback|error`) - API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and return `source` field (`directus|fallback|error`)
### n8n Integration ### n8n Integration
- Webhook base URL: `N8N_WEBHOOK_URL` env var
- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers - Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image)
- All endpoints have rate limiting and 10s timeout protection - Auth: `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
- Hardcover reading data cached for 5 minutes - All endpoints have rate limiting and 10s timeout
- Hardcover reading data cached 5 minutes
## Key Conventions ## Key Conventions
### i18n (Internationalization) ### i18n
- **Supported locales**: `en` (English), `de` (German)
- **Primary source**: Static JSON files in `messages/en.json` and `messages/de.json`
- **Optional override**: Directus CMS `messages` collection
- **Server components**: Use `getHeroTranslations()`, `getNavTranslations()`, etc. from `lib/translations-loader.ts`
- **Client components**: Use `useTranslations("key.path")` from next-intl
- **Locale mapping**: Middleware defines `["en", "de"]` which must match `app/[locale]/layout.tsx`
### Component Patterns - Locales: `en`, `de` — defined in `middleware.ts`, must match `app/[locale]/layout.tsx`
- **Client components**: Mark with `"use client"` for interactive/data-fetching parts - Client components: `useTranslations("key.path")` from `next-intl`
- **Data loading**: Use `useEffect` for client-side fetching on mount - Server components: `getTranslations("key.path")` from `next-intl/server`
- **Animations**: Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp` - Always add keys to both `messages/en.json` and `messages/de.json`
- **Loading states**: Every async component needs a matching Skeleton component
### Design System ("Liquid Editorial Bento") ### Design System
- **Core palette**: Cream (`#fdfcf8`), Stone (`#0c0a09`), Emerald (`#10b981`)
- **Custom colors**: Prefixed with `liquid-*` (sky, mint, lavender, pink, rose, peach, coral, teal, lime)
- **Card style**: Gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`)
- **Glassmorphism**: Use `backdrop-blur-sm` with `border-2` and `rounded-xl`
- **Typography**: Headlines uppercase, tracking-tighter, with accent point at end
- **Layout**: Bento Grid for new features (no floating overlays)
### File Naming - Custom Tailwind colors: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
- **Components**: PascalCase in `app/components/` (e.g., `About.tsx`) - Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm`, `border-2`, `rounded-xl`
- **API routes**: kebab-case directories in `app/api/` (e.g., `book-reviews/`) - Typography: Headlines uppercase, `tracking-tighter`, accent dot at end (`<span className="text-emerald-600">.</span>`)
- **Lib utilities**: kebab-case in `lib/` (e.g., `email-obfuscate.ts`) - Layout: Bento Grid, no floating overlays
- Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400`) for body text — contrast ratio must be ≥4.5:1
### Code Style ### Code Style
- **Language**: Code in English, user-facing text via i18n
- **TypeScript**: No `any` types - use interfaces from `lib/directus.ts` or `app/_ui/`
- **Error handling**: All API calls must catch errors with fallbacks
- **Error logging**: Only in development mode (`process.env.NODE_ENV === "development"`)
- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`)
- **No emojis**: Unless explicitly requested
### Testing Notes - TypeScript: no `any` — use interfaces from `lib/directus.ts` or `types/`
- **Jest environment**: JSDOM with mocks for `window.matchMedia` and `IntersectionObserver` - Error logging: `console.error` only when `process.env.NODE_ENV === "development"`
- **Playwright**: Uses plain Next.js dev server (no Docker) with `NODE_ENV=development` to avoid Edge runtime issues - File naming: PascalCase components (`About.tsx`), kebab-case API routes (`book-reviews/`), kebab-case lib utils
- **Transform**: ESM modules (react-markdown, remark-*, etc.) are transformed via `transformIgnorePatterns` - Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`)
- **After UI changes**: Run `npm run test` to verify no regressions - Every async component needs a Skeleton loading state
### Testing
- Jest with JSDOM; mocks for `window.matchMedia` and `IntersectionObserver` in `jest.setup.ts`
- ESM modules transformed via `transformIgnorePatterns` (react-markdown, remark-*, etc.)
- 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
### Docker & Deployment ### Docker & Deployment
- **Standalone mode**: `next.config.ts` uses `output: "standalone"` for optimized Docker builds
- **Branches**: `dev` → staging, `production` → live - `output: "standalone"` in `next.config.ts`
- **CI/CD**: Gitea Actions (`.gitea/workflows/`) - Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal on failure), starts server
- **Verify Docker builds**: Always test Docker builds after changes to `next.config.ts` or dependencies - CI/CD: `.gitea/workflows/ci.yml``test-build` job (all branches), `deploy-dev` (dev only), `deploy-production` (production only)
- Branches: `dev` → testing.dk0.dev, `production` → dk0.dev
- Dev and production share the same PostgreSQL and Redis instances
## Common Tasks ## Common Tasks
### Adding a CMS-managed section ### Adding a CMS-managed section
1. Define GraphQL query + types in `lib/directus.ts` 1. Define GraphQL query + types in `lib/directus.ts`
2. Create API route in `app/api/<name>/route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'` 2. Create API route in `app/api/<name>/route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'`
3. Create component in `app/components/<Name>.tsx` 3. Create component in `app/components/<Name>.tsx` with Skeleton loading state
4. Add i18n keys to `messages/en.json` and `messages/de.json` 4. Add i18n keys to both `messages/en.json` and `messages/de.json`
5. Integrate into parent component 5. Create a `<Name>Client` wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider`
6. Add to `HomePageServer.tsx` wrapped in `ScrollFadeIn`
### Adding i18n strings
1. Add keys to both `messages/en.json` and `messages/de.json`
2. Use `useTranslations("key.path")` in client components
3. Use `getTranslations("key.path")` in server components
### Working with Directus
- All queries go through `directusRequest()` in `lib/directus.ts`
- Uses GraphQL endpoint (`/graphql`) with 2s timeout
- Returns `null` on failure (graceful degradation)
- Translations filtered by `languages_code.code` matching Directus locale
## Environment Variables
### Required for CMS
```bash
DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=...
```
### Required for n8n features
```bash
N8N_WEBHOOK_URL=https://n8n.dk0.dev
N8N_SECRET_TOKEN=...
N8N_API_KEY=...
```
### Database & Cache
```bash
DATABASE_URL=postgresql://...
REDIS_URL=redis://...
```
### Optional
```bash
NEXT_PUBLIC_BASE_URL=https://dk0.dev
```
## Documentation References
- Operations guide: `docs/OPERATIONS.md`
- Locale system: `docs/LOCALE_SYSTEM.md`
- CMS guide: `docs/CMS_GUIDE.md`
- Testing & deployment: `docs/TESTING_AND_DEPLOYMENT.md`