This site — a self-hosted portfolio with a real pipeline
An animation-forward portfolio that treats itself as a production system: a shader-drawn aurora, architecture diagrams that draw themselves and a live demo you can watch cross the perimeter — behind staging and production environments, automatic TLS, CI that lints, typechecks, builds and runs Playwright user journeys before every deploy, advisory Lighthouse budgets, and a rate-limited server route behind the live egress-proxy demo.
Architecture
- 01
Author
Next.js App Router, TypeScript, typed content files instead of a CMS
- 02
Animate
GSAP for choreography, a non-blocking Web Animations API fade for route changes, one WebGL2 shader for the aurora; all reduced-motion aware, a second animation library was measured at 75% unused and removed, and a first attempt at route transitions (React's ViewTransition) was measured at 600-900ms per click and replaced
- 03
Verify
lint, typecheck, build and Playwright user journeys on every push; Lighthouse and visual diffs advisory
- 04
Build
once, in CI: standalone output, smoke-tested over real HTTP, promoted to a deploy branch
- 05
Ship
Coolify copies that tree into a runtime-only image: develop to staging, main to production
The problem
A portfolio is a production system with one user who matters and no second chance at a first impression. It should demonstrate front-end craft, performance discipline and real deployment practice — not just list them.
Approach & decisions
- Content is visible before JavaScript runs, and every animation is gated behind prefers-reduced-motion. prefers-reduced-transparency turns the glass surfaces solid. Nothing on the page depends on an animation firing.
- One animation engine. GSAP does the choreography — masked line and character reveals, the scroll-drawn architecture diagrams (DrawSVG and MotionPath), the pinned scroll story, magnetic buttons and card tilt. Route changes first used React's experimental ViewTransition component, sharing element names so a card morphed into its case-study header — but measured, it added 600-900ms of dead time to every navigation, including ones with nothing to morph: React's own docs note it waits up to 500ms for new fonts before animating, a cost the App Router's per-route CSS chunks triggered on every click. It's now a short, non-blocking entrance fade on the Web Animations API; in the bisection, bypassing ViewTransition alone took the same clicks from 600-900ms to 95-150ms.
- The aurora behind every page is one WebGL2 fragment shader, not a library: three curtains of light on fractal noise, rendered at half resolution, capped at 30 fps and one frame under reduced motion. It measures its own first frames and, on a software renderer or a struggling GPU, hands back to the CSS version rather than cost the main thread.
- The live demo on the egress-proxy case study calls the deployed proxy from a server-side route, not from the browser — no CORS exposure, a per-IP rate limit, an input cap, and the upstream address kept in configuration rather than the client bundle.
- Staging is noindexed by header and robots rule, so the test site is never the one a recruiter finds by accident.
- Build once, promote the artifact. The first on-host build stalled the shared server under memory pressure; rather than buy a bigger box, the pipeline was reshaped so the bytes CI verified are the bytes that ship, and a deploy can never compete with production for memory.
- The interactive WebGL portrait reveal lives on a branch, deliberately unshipped until its calibration is finished. A working, accessible hero beats an impressive one that never leaves the branch.
Results
- Two environments, one push each — develop deploys to staging, main to production — with automatic Let's Encrypt TLS.
- The serving host never compiles. next build peaks at about 1.5 GB, more than a shared 8 GB box has spare, so CI builds once and promotes the verified tree; the host's only job is a COPY.
- Zero additional hosting cost: it reuses an existing Hetzner server.
- Honours prefers-reduced-motion, prefers-reduced-transparency and forced-colors, with every piece of content visible before JavaScript runs — the diagrams render complete, the demo shows its result at once, and route changes become a plain swap: the route fade checks the media query in script, since a Web Animations API animation is out of reach of the stylesheet's reduced-motion rule.
- The live demo's animation is driven by the real response: the packet only carries on to the vendor when the proxy forwarded, and is bounced back when it refused. The picture and the proof cannot disagree.