~4 minutes to read case study

Shipped June 2026 · source on GitHub

Case Study: Rebuilding this site from a broken Safari tab to a Web-Components portfolio

The Break

One day this site simply stopped working on iPhones. The diagnosis was embarrassing and instructive: the homepage was literally a CodePen iframe — a demo of CSS scroll-driven animations (animation-timeline, view-timeline) that only Chrome had ever shipped. Safari got a half-applied fallback referencing a polyfill that never loaded. The pen's own README admitted it: "Currently only works on Chrome desktop."

Around that iframe sat two years of accumulated React: a Vike SSR app with 56 components — some duplicated wholesale (superCursor/ AND superCursor__/, neither ever imported), a smooth-scroll library that was installed but never wired, and a 649-line case-study page. It was built fast in the early AI-assisted era, moving too quickly to look back. The deploy process was hand-built Docker images pushed to a registry, then SSH into a droplet to run a script.

The Decision: No Framework

The rebuild bet was that a portfolio doesn't need a framework — it needs to be fast, reliable everywhere, and made of pieces anyone can steal. The stack became:

  1. Vanilla HTML pages built as a Vite multi-page app — every page is real HTML at its URL, which makes SEO trivial instead of an SSR engineering project.
  2. Web Components in light DOM — every interactive piece is one JS file plus one CSS file. No shadow DOM fighting the cascade, no build-time magic. Copy two files into any project and it works.
  3. GSAP for scroll-driven visuals (it went 100% free, plugins included) riding on native CSS scroll-snap — the browser owns the scroll feel, JavaScript only paints. The Chrome-only effect now runs identically in every browser, including the iPhone that started this.
  4. A ~170-line Express server — stateless HMAC-signed cookies for the gated case studies, a hand-rolled rate limiter, and a contact endpoint. That is the entire backend.

What Got Deleted

Six thousand six hundred lines of React, the SSR renderer, the smooth-scroll library, the Docker pipeline, a paid GSAP registry token, and the iframe. This site had always lived in a private repo; going public for the first time meant streamlining everything — the history restarted as a single clean commit, the codebase slimmed to exactly what ships, and the whole thing became something worth reading: a return to the elegance and simplicity of HTML and CSS, on display.

Going Public Without Breaking NDAs

One genuinely interesting problem: the gated client case studies can't sit readable in a public repo — an auth wall on the website means nothing if the source is browsable on GitHub. The solution keeps the repo open and the obligations intact:

  1. The five client studies — pages, their built asset chunks, every screenshot — are encrypted locally with AES-256-GCM into a content-locked/ directory. That ciphertext is what gets committed. Clone the repo and you get working everything, plus 229 files of noise.
  2. The decryption key lives only in the deployment platform's secret store (and on my machine).
  3. At request time, the server checks the visitor's signed auth cookie, decrypts the page from the locked store, and serves it — with an in-memory cache so the crypto runs once, not per request.
  4. Cloud builds never see plaintext at all: the build config includes the gated pages only when their files exist, so the same repo builds completely on a machine that holds the content and a CI runner that doesn't.

Password-holders see case studies. Everyone else sees the architecture that protects them — which is arguably the better portfolio piece.

The Gotchas (kept honest)

Every rebuild has a folklore section. These are real, and each one cost an iteration:

  1. The font that wasn't the font. The menu's typeface was "Mona-Sans" via a CDN whose stylesheet declares several width cuts at the same weight — and in a @font-face cascade, the last one wins. The old site had been silently rendering Mona Sans Black Wide the whole time. Self-hosting the "same font" looked wrong until the exact wide cuts were matched.
  2. Safari's calc() cannot divide by a CSS variable. calc(33vw * var(--h) / var(--w)) silently invalidates; the homepage images fell back to intrinsic size and crushed the text wrap. Chrome was fine, which made it look like a layout bug instead of a parser one. Fix: pre-compute the ratios to literals.
  3. The old app was content-box. Its reset never set box-sizing: border-box globally, so every "32px" header was actually 48px. Porting dimensions verbatim shrank the chrome.
  4. DigitalOcean App Platform blocks outbound SMTP. Entirely. The Nodemailer/Gmail contact form hung into 504s in production while working perfectly locally. Email moved to an HTTPS API (Resend) — DKIM-signed from this domain.
  5. Blur is a haunted property. A blurred element's pixels spread beyond its box (Safari shows the bleed as a ghost glow); a will-change hint caches the soft mid-animation raster; and a scroll-snap landing at scale 0.999 leaves the whole layer permanently bitmap-fuzzy. The melt effect now blurs only what sells it, hints nothing, and snaps its scale to exactly 1.0.
  6. GSAP pinning inside flex wants blood. The pin-spacer reflows flex siblings — the old site fought it with !important hacks. The "photo holds while text scrolls" effect is now four lines of position: sticky.

The Numbers

  1. React/Vike code deleted: ~6,600 lines — replaced by zero framework code
  2. Reusable elements shipped: 20+ (galleries, loaders, neon signs, rain, a basketball)
  3. Homepage JavaScript: ~50KB gzipped, GSAP included
  4. Hosting: $5/month, deployed by git push
  5. Browsers that get the full experience: all of them

The Process: Pair-Built with Claude

This rebuild was a sustained collaboration with Claude (Opus 4.8) in Claude Code. The shape of the work: Claude explored the old codebase and proposed the architecture; the migration ran as planned milestones with real commits; sub-agents ported entire case-study pages in parallel; and every visual call — the snap feel, the glow intensity, a 220px-versus-260px image offset — was a human judgment fed back from a real browser and a real phone. The division of labor was honest: AI hands moved fast through six thousand lines; human eyes caught every place where fast wasn't right. The same collaboration wrote the words you're reading.

Outcome

The site that broke on an iPhone is now verified on one. The portfolio is its own proof-of-work: view source, read the repository, take whatever's useful — it's MIT. Everyone wanted to see how I did it. Here it is.

"If a picture is worth a thousand words —
a prototype is worth a thousand meetings."