- 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>
5.5 KiB
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 viaPromise.all, renders Hero directly, wraps client sections inScrollFadeInapp/components/Hero.tsx— server component (no"use client"), usesgetTranslations()fromnext-intl/serverapp/components/ClientWrappers.tsx— exportsAboutClient,ProjectsClient,ContactClient,FooterClient, each wrapping their component in a scopedNextIntlClientProviderwith only the needed translation keysapp/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 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:
- 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, usesdirectusRequest()with 2s timeout - Returns
nullon failure (never throws) - Locale mapping:
en→en-US,de→de-DE - API routes must export
runtime = 'nodejs',dynamic = 'force-dynamic', and returnsourcefield (directus|fallback|error)
n8n Integration
- Webhook proxies in
app/api/n8n/(status, chat, hardcover, generate-image) - Auth:
N8N_SECRET_TOKENand/orN8N_API_KEYheaders - All endpoints have rate limiting and 10s timeout
- Hardcover reading data cached 5 minutes
Key Conventions
i18n
- Locales:
en,de— defined inmiddleware.ts, must matchapp/[locale]/layout.tsx - Client components:
useTranslations("key.path")fromnext-intl - Server components:
getTranslations("key.path")fromnext-intl/server - Always add keys to both
messages/en.jsonandmessages/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-*/15withbackdrop-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(nottext-stone-400) for body text — contrast ratio must be ≥4.5:1
Code Style
- TypeScript: no
any— use interfaces fromlib/directus.tsortypes/ - Error logging:
console.erroronly whenprocess.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.matchMediaandIntersectionObserverinjest.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: useeslint-disable-next-line @next/next/no-img-elementon the<img>tag
Docker & Deployment
output: "standalone"innext.config.ts- Entrypoint:
scripts/start-with-migrate.js— waits for DB, runs migrations (non-fatal on failure), starts server - CI/CD:
.gitea/workflows/ci.yml—test-buildjob (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
- Define GraphQL query + types in
lib/directus.ts - Create API route in
app/api/<name>/route.tswithruntime='nodejs'anddynamic='force-dynamic' - Create component in
app/components/<Name>.tsxwith Skeleton loading state - Add i18n keys to both
messages/en.jsonandmessages/de.json - Create a
<Name>Clientwrapper inClientWrappers.tsxwith scopedNextIntlClientProvider - Add to
HomePageServer.tsxwrapped inScrollFadeIn