Attach portfolio-app-dev to the edge network on every deploy and default the dev base URL to https://testing.dk0.dev. Route lives in the homelab repo (apps/doxx/traefik/portfolio-testing.yml).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AI chat moves into a window/sheet, opened from a new speech-bubble card on
the desk; it now respects the chatbot consent and asks before sending
anything to n8n (previously it ignored the consent)
- Contact as red poster section with big mail address and the form on a paper
card; validation errors are finally shown inline (aria-invalid, role=alert)
- Footer as ink strip with dk0 wordmark, legal links and a button to reopen
the privacy settings
- "About me" in the header opens the about window on the home page
- Remove the old About section and its parts (BentoChat, ActivityFeed,
CurrentlyReading, ReadBooks), the unused ChatWidget, Projects and BentoGrid,
their tests, wrappers, loaders, types and texts
- scripts/directus-cleanup-redesign.mjs: dry run by default, --apply archives
hobbies, tech stack categories and old content pages and deletes four
unused message overrides after a full JSON backup; run only after the
redesign is live on production
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tapping an object on the desk opens a window (desktop) or a bottom sheet
(mobile) instead of flipping: projects, now, servers, reading, about, contact
- Desktop windows in poster style (2px ink rule, hard offset shadow), open
from the tapped object, thrown by the title bar with momentum and elastic
viewport edges, stack by focus, minimize into a tray, Escape closes the top
window, focus moves to close and returns to the trigger
- Mobile sheets via vaul 1.1.2: drag down to dismiss, overlay, focus trap
- Project window shows live/source links from Directus when present
- Hero "Write me" opens the contact window; still a plain #contact link
without JavaScript
- Shared current-book loader for desk, deck and reading window
- Texts in de/en, i18n key check extended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New "Tisch" section replaces the old projects block on the home page
- Objects: project cards from Directus (flip on tap, link to detail page),
thesis ticket with countdown, server card, currently-reading book from
Hardcover (with skeleton), polaroid photo, sticker
- Desktop: Framer Motion drag with momentum, elastic table edges, tilt that
follows throw velocity, bring-to-front, "Tidy up" and "Mess it up"
- Mobile: card deck, swipe the top card away (distance or velocity),
prev/next buttons, live counter, tap to flip
- Both layouts are server rendered and switched by CSS, so content is
visible without JavaScript and nothing jumps on hydration
- Taps after a drag no longer flip cards
- New named colors polaroid/bookcover instead of raw hex classes
- Texts in de/en, i18n key check extended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New design tokens: signal red, ink and paper as RGB channels; ink and paper
swap roles in dark mode so every component inverts cleanly
- Fonts via next/font: Bricolage Grotesque (with wdth/opsz axes), Geist,
Geist Mono; drop Inter and Playfair
- Poster hero: kicker, giant DK0 wordmark that reacts to pointer distance
(weight and width through a damped spring, desktop only, off for reduced
motion), claim as h1, intro with thesis countdown, marquee strip
- Header in poster style: transparent over the red hero, paper strip with
ink rule when scrolled or on subpages; drawer menu restyled
- Remove wavy SVG separators, CSS shader gradient, background blobs and
unused providers that caused the pink/purple bands between sections
- Drop the global p/span/div color override so colors inherit properly
- New hero texts in de/en, tests and i18n key check updated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Override Tailwind ease-out/ease-in-out with stronger curves, add ease-drawer
- hoverOnlyWhenSupported so hover styles never stick on touch devices
- Replace transition-all with explicit properties across the public UI
- Press feedback via active:scale-[0.97] instead of hover:scale-105
- Global prefers-reduced-motion handling (CSS + MotionConfig reducedMotion="user")
- Mobile menu is a real drawer: stays mounted, inert when closed, Escape closes
- Progress and music bars animate scaleX/scaleY instead of width/height
- Fix undefined animate-spin-slow, contain hero CTA shine with overflow-hidden
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add locale-specific title/description for DE and EN homepage
- Expand keywords with local SEO terms (Webentwicklung Osnabrück, Informatik, etc.)
- Add WebSite schema and enhance Person schema with knowsAbout, alternateName
- Add hreflang alternates for DE/EN
- Update projects page with locale-specific metadata
- Keep visible titles short, move SEO terms to description/structured data
- Book reviews: add line-clamp for longer review text with expand/collapse per review
- Privacy policy: restore full detailed DSGVO-compliant fallback content
- Header (legal pages): change logo from 'dk' to 'dk0' in circle
- Header (main page): add ThemeToggle for dark/light mode switching
- Skeleton loading: integrate boneyard-js for ReadBooks, CurrentlyReading, Projects
- Add boneyard.config.json and bones/registry.ts placeholder
- Add modal popup to view complete book reviews
- Click 'Read full review' opens animated modal
- Responsive design optimized for mobile and desktop
- Liquid design system styling with gradients and blur effects
- Modal includes book cover, rating, and full review text
- Close via X button or backdrop click
- Smooth Framer Motion animations
- Clean up old n8n workflow temporary files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: pass locale explicitly to Hero and force-dynamic on locale-sensitive API routes
- Hero.tsx: pass locale prop directly to getTranslations instead of
relying on setRequestLocale async storage, which can be lost during
Next.js RSC streaming
- book-reviews route: replace revalidate=300 with force-dynamic to
prevent cached English responses being served to German locale requests
- content/page route: add runtime=nodejs and force-dynamic (was missing
both, violating CLAUDE.md API route conventions)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: scroll to top on locale switch and remove dashes from hero text
- HeaderClient: track locale prop changes with useRef and call
window.scrollTo on switch to reliably reset scroll position
- messages/en.json + de.json: replace em dash with comma and remove
hyphens from Self-Hoster/Full-Stack in hero description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Initial plan
* feat: improve Snippets/The Lab UI with category filters, search, language badges and code preview
Agent-Logs-Url: https://github.com/denshooter/portfolio/sessions/4a562022-cad7-4b4f-8bc4-1be022ecbf1e
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>
---------
Co-authored-by: denshooter <dennis@konkol.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>
Notification email (to Dennis):
- Complete dark-theme redesign: #0c0c0c bg, #141414 card, gradient top bar
- Sender avatar with liquid-mint/sky gradient + initial letter
- Subject displayed as pill badge
- Message in styled blockquote with mint left border
- Gradient "Direkt antworten" CTA button
- replyTo header already set so email Reply goes directly to sender
Telegram notification:
- sendTelegramNotification() fires after successful email send (fire-and-forget)
- Uses TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID env vars (silently skips if absent)
- HTML-formatted message with emojis, name/email/subject/message preview
- Inline keyboard button "Per E-Mail antworten" with pre-filled mailto link
- Never blocks the contact form response if Telegram fails
Reply email templates (respond/route.tsx):
- Same dark design system as notification email
- baseEmail() generates consistent header + footer
- messageCard() helper for styled message blocks with colored left border
- ctaButton() helper for gradient CTA buttons
- Templates: welcome, project, quick, reply — all updated to dark theme
Required new env vars:
TELEGRAM_BOT_TOKEN=<from @BotFather>
TELEGRAM_CHAT_ID=<your chat/user ID>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Hero server component awaited getMessages(locale) which called Directus
with a 2-second timeout. On testing.dk0.dev (or when Directus is unreachable),
this blocked the entire Hero render for ~2s → LCP 3.0s / 2320ms rendering delay.
Changes:
- Hero.tsx: remove getMessages() call entirely; use t() for all strings
- messages/en.json + de.json: add hero.badge, hero.line1, hero.line2 keys
- lib/i18n-loader.ts: invert lookup order — JSON first, Directus only as
override for keys absent from JSON. Previously Directus was tried first
for every key, causing ~49 parallel network requests per page load in
HomePageServer (aboutT + projectsT + contactT + footerT translations).
Now all JSON-backed keys return instantly without any network I/O.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace next-themes (38 KiB) with a tiny custom ThemeProvider (~< 1 KiB)
using localStorage + classList.toggle for theme management
- Add FOUC-prevention inline script in layout.tsx to apply saved theme
before React hydrates
- Remove framer-motion from Header.tsx: nav entry now uses CSS slideDown
keyframe, mobile menu uses CSS opacity/translate transitions
- Remove framer-motion from ThemeToggle.tsx: use Tailwind hover/active scale
- Remove framer-motion from legal-notice and privacy-policy pages
- Update useTheme import in ThemeToggle to use custom ThemeProvider
- Add slideDown keyframe to tailwind.config.ts
- Update tests to mock custom ThemeProvider instead of next-themes
Result: framer-motion moves from "First Load JS shared by all" to lazy
chunks; next-themes chunk eliminated entirely; -38 KiB from initial bundle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
overflow-hidden on the <section> was clipping the -bottom-6 badge
and the image bottom on iPad viewports where content sits near the
section edge. Move overflow-hidden to the blobs container (absolute
inset-0) so the blobs are still clipped but the image and badge can
render freely. Add pb-10 sm:pb-16 bottom padding so the badge always
has clearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TipTap (ProseMirror) was causing:
- chunks 1007 (85 KiB) and 3207 (58 KiB) in the initial bundle
- Array.prototype.at/flat/flatMap, Object.fromEntries/hasOwn polyfills
(ProseMirror bundles core-js for these — the 12 KiB legacy JS flag)
- 2+ seconds of main thread blocking on mobile
Fix: move HTML conversion to the server (API route) and pass the
resulting HTML string to the client, eliminating the need to import
richTextToSafeHtml (and transitively TipTap) in any client component.
Changes:
- app/api/content/page/route.ts: call richTextToSafeHtml server-side,
add html: string to response alongside existing content
- app/components/RichTextClient.tsx: accept html string, remove all
TipTap imports — TipTap/ProseMirror now has zero client bundle cost
- app/components/About.tsx, Contact.tsx: use cmsHtml from API
- app/legal-notice/page.tsx, privacy-policy/page.tsx: same
- app/components/ClientWrappers.tsx: change static imports of About,
Projects, Contact, Footer to next/dynamic so their JS is in
separate lazy-loaded chunks, not in the initial bundle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>