Files
portfolio/.github/copilot-instructions.md
denshooter 4a8cb5867f
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
docs: update copilot instructions with SSR patterns and CI/CD changes
- 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>
2026-03-04 23:47:17 +01:00

5.5 KiB

Portfolio Project Instructions

Dennis Konkol's portfolio (dk0.dev) — Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system.

Build, Test, and Lint

npm run dev:next           # Plain Next.js dev server (no Docker)
npm run build              # Production build (standalone mode)
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 client 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 wrapping their component in a scoped NextIntlClientProvider with only the needed translation keys
  • 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 hydration fails.

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.

Framer Motion 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 → 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)
  • Locale mapping: enen-US, dede-DE
  • API routes must export runtime = 'nodejs', dynamic = 'force-dynamic', and return source field (directus|fallback|error)

n8n Integration

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

Key Conventions

i18n

  • Locales: en, de — defined in middleware.ts, must match app/[locale]/layout.tsx
  • Client components: useTranslations("key.path") from next-intl
  • Server components: getTranslations("key.path") from next-intl/server
  • Always add keys to both messages/en.json and messages/de.json

Design System

  • Custom Tailwind colors: 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>)
  • 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

  • TypeScript: no any — use interfaces from lib/directus.ts or types/
  • Error logging: console.error only when process.env.NODE_ENV === "development"
  • File naming: PascalCase components (About.tsx), kebab-case API routes (book-reviews/), kebab-case lib utils
  • Commit messages: Conventional Commits (feat:, fix:, chore:)
  • 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

  • output: "standalone" in next.config.ts
  • Entrypoint: scripts/start-with-migrate.js — waits for DB, runs migrations (non-fatal on failure), starts server
  • CI/CD: .gitea/workflows/ci.ymltest-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

Adding a CMS-managed section

  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'
  3. Create component in app/components/<Name>.tsx with Skeleton loading state
  4. Add i18n keys to both messages/en.json and messages/de.json
  5. Create a <Name>Client wrapper in ClientWrappers.tsx with scoped NextIntlClientProvider
  6. Add to HomePageServer.tsx wrapped in ScrollFadeIn