CI:
- Replace `npm run build` with `tsc --noEmit` in test-build job
→ eliminates duplicate Next.js build (~5 min saved per push)
→ Docker deploy job already does the full build
PageSpeed:
- Add `cssChunking: 'loose'` to merge CSS into one chunk and break
the 84dc7384→3aefc04b render-blocking CSS waterfall chain (450ms mobile)
- Remove @shadergradient/react, @react-three/fiber, three from
package.json — packages were already unused in code, removes any
residual bundling risk for chunk 7001
Hero:
- Change lg:flex-row → xl:flex-row so iPad (1024px) stays in column
layout; the 9.5rem heading overflowed into the image at lg causing
the photo to be clipped by overflow-hidden on the section
- Update image sizes attribute to match new xl breakpoint
- Fix contrast: "GET IN TOUCH" link text-stone-500 → text-stone-700
(contrast 3.7:1 → 7:1, now WCAG AA compliant)
- Change text-center/justify-center to xl: variants to match layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the missing env vars to deploy-dev so testing.dk0.dev has
access to Directus CMS data (projects, books) and n8n features,
matching the production container configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add POST /api/n8n/hardcover/sync-books — n8n calls this after detecting
finished books in Hardcover. Authenticates via N8N_SECRET_TOKEN/N8N_API_KEY,
deduplicates by hardcover_id, creates new book_reviews entries in Directus.
- Add getBookReviewByHardcoverId() + createBookReview() to lib/directus.ts.
Check uses GraphQL filter; create uses Directus REST POST /items/book_reviews.
- ReadBooks: replace silent return null with a visible empty state so the
section stays visible with a hint until the n8n sync populates it.
- Projects: add "No projects yet." placeholder instead of blank grid when
both Directus and PostgreSQL return no data.
- Add home.about.readBooks.empty i18n key (EN + DE).
n8n workflow setup:
Schedule → HTTP Hardcover GraphQL (books_read) → Code (transform) →
POST /api/n8n/hardcover/sync-books with array of { hardcover_id, title,
author, image, rating, finished_at }
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
requestIdleCallback is unavailable in Safari < 16.4, causing GatedProviders
to crash during hydration and blank the entire page. Added setTimeout fallback.
Also added IntersectionObserver fallback in ScrollFadeIn for safety.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ScrollFadeIn uses IntersectionObserver + CSS transitions instead of
Framer Motion's initial prop. Key difference: no inline style in SSR
HTML, so content is visible by default. Animation only activates
after client hydration (hasMounted check).
Wraps About, Projects, Contact, Footer in HomePageServer.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Framer Motion's initial={{ opacity: 0 }} was rendered as inline
style='opacity:0' in SSR HTML. If client-side JS failed to hydrate
properly, sections stayed permanently invisible.
Removed whileInView scroll animations from About, Projects, Contact.
Modal animations (AnimatePresence) kept as they only render on interaction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dynamic() caused Framer Motion's initial opacity:0 to be baked into
SSR HTML, but client-side hydration never triggered the animations.
Direct imports ensure Framer Motion properly takes over on hydration.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ssr:false caused sections to only render client-side, making them
invisible if any JS error occurred. Keep dynamic() for code-splitting
but allow server-side rendering.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dev-deploy workflow was trying to spin up separate portfolio_postgres_dev
and portfolio_redis_dev containers, which don't exist on the server. Now it
reuses the existing production portfolio-postgres and portfolio-redis on
the portfolio_net network.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mount .next/cache as a BuildKit cache volume during build to persist
the Next.js build cache across Docker rebuilds. Eliminates the
'No build cache found' warning and speeds up subsequent builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make prisma migrate deploy failure non-fatal in start-with-migrate.js.
Previously, migration failure caused process.exit() which killed the
container, triggering an infinite restart loop. Now logs a warning
and starts the Next.js server anyway (app has DB fallbacks).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove @sentry/nextjs and all related files since it was never actively used.
- Delete sentry.server.config.ts, sentry.edge.config.ts
- Delete sentry-example-page and sentry-example-api routes
- Clean up instrumentation.ts, global-error.tsx, middleware.ts
- Remove Sentry env vars from env.example and docs
- Update CLAUDE.md, copilot-instructions.md, PRODUCTION_READINESS.md
Middleware bundle reduced from 86KB to 34.8KB (-51KB).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove framer-motion from Hero.tsx and HeaderClient.tsx, replace with CSS animations/transitions
- Replace lucide-react icons (Menu, X, Mail) with inline SVGs in HeaderClient.tsx
- Lazy-load About, Projects, Contact, Footer via dynamic() imports in ClientWrappers.tsx
- Defer ShaderGradient/BackgroundBlobs loading via requestIdleCallback in ClientProviders.tsx
- Remove AnimatePresence page wrapper that caused full re-renders
- Enable experimental.optimizeCss (critters) for critical CSS inlining
- Add fadeIn keyframe to Tailwind config for CSS-based animations
Homepage JS reduced from 563KB to 438KB (-125KB).
Eliminates ~39s main thread work from WebGL init and layout thrashing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add WebGL support detection in ShaderGradientBackground to prevent console errors
- Add .catch() fallback to ShaderGradientBackground dynamic import
- Remove hardcoded aria-label from consent banner minimize button (fixes label-content-name-mismatch)
- Use rewrite instead of redirect for root locale routing (eliminates one redirect hop)
- Change n8n API cache headers from no-store to no-cache (enables bfcache)
- Add three and @react-three/fiber to optimizePackageImports for better tree-shaking
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Disable Sentry in all 3 configs (client/server/edge) - replayIntegration
was recording every DOM mutation causing overhead in Chrome
- Remove grain-overlay div and its CSS (SVG feTurbulence + mix-blend-mode:overlay
forces software compositing in Chrome on every frame)
- Remove mix-blend-multiply from BackgroundBlobs (prevents Chrome GPU compositing)
- Delete unused Grain.tsx, ShaderGradientBackground.tsx and its client wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ShaderGradientBackground used 3 full-screen Three.js WebGL canvases
with a blur(150px) CSS filter, crashing Lighthouse and causing severe
lag in Chrome. BackgroundBlobs also had 7 elements with blur(100-120px)
and per-frame mouse spring tracking compounding the issue.
- Remove ShaderGradientBackground from layout (WebGL not needed for a blur effect)
- Reduce BackgroundBlobs blur from 100-120px to 60px
- Remove mouse tracking spring animations from BackgroundBlobs
- Reduce to 4 blobs (remove 3 least visible)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
next/dynamic with ssr:false is not allowed in Server Components.
Follows existing BackgroundBlobsClient pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `prefix` param to checkRateLimit/getRateLimitHeaders so each endpoint
has its own bucket (previously all shared `admin_${ip}`, causing 429s when
analytics/track incremented past n8n endpoints' lower limits)
- n8n/hardcover/currently-reading → prefix 'n8n-reading'
- n8n/status → prefix 'n8n-status'
- analytics/track → prefix 'analytics-track'
- Remove custom analytics system (AnalyticsProvider, lib/analytics,
lib/useWebVitals, all /api/analytics/* routes) — was causing 500s in
production due to missing PostgreSQL PageView table
- Remove analytics consent toggle from ConsentBanner/ConsentProvider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove hardcoded Dennis Konkol idle quote from rotation
- Double quote pool (5 → 12 quotes per locale)
- Start at a random quote on page load
- Cycle to a random non-repeating quote every 10s instead of sequential
- Fix dev-deploy.yml: postgres:15-alpine → postgres:16-alpine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@shadergradient/react imports these at runtime even though they are not
declared as peer dependencies in its package.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove @react-three/drei, @react-three/fiber, three, @types/three
(replaced by @shadergradient/react), plus gray-matter, zod,
react-responsive-masonry and related @types packages that are
not imported anywhere in the codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker Compose refused to adopt the existing portfolio_net network because
it lacked the expected com.docker.compose.network label (created outside
Compose). Mark it as external (matching the dev setup) and pre-create it
in the deployment workflow to ensure it always exists before compose up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The portfolio_dev network was created manually by the pipeline, causing
docker-compose to fail with label mismatch errors. Now:
- Network is marked as external in compose (compose doesn't try to own it)
- Network creation moved before compose up in the pipeline
- Redundant network check later in pipeline removed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- start-with-migrate.js now waits for the database TCP port to be
reachable before running Prisma migrations (15 retries, 2s interval).
Prevents the container from crashing and restarting in a loop when
postgres is still starting up.
- Add explicit 'name:' to both production and dev compose networks
to prevent docker-compose project prefix mismatch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Docker Compose prefixes network names with the project name by default.
The app container (started via docker run) was connecting to 'portfolio_dev'
while postgres/redis were on '<project>_portfolio_dev' - different networks.
Setting 'name: portfolio_dev' forces the exact name so all containers
share the same network.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>