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:
- 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.
- 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.
- 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.
- 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:
- 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. - The decryption key lives only in the deployment platform's secret store (and on my machine).
- 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.
- 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:
- 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-facecascade, 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. - 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. - The old app was content-box. Its reset never set
box-sizing: border-boxglobally, so every "32px" header was actually 48px. Porting dimensions verbatim shrank the chrome. - 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.
- Blur is a haunted property. A blurred element's pixels spread beyond its box (Safari
shows the bleed as a ghost glow); a
will-changehint 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. - GSAP pinning inside flex wants blood. The pin-spacer reflows flex siblings — the old
site fought it with
!importanthacks. The "photo holds while text scrolls" effect is now four lines ofposition: sticky.
The Numbers
- React/Vike code deleted: ~6,600 lines — replaced by zero framework code
- Reusable elements shipped: 20+ (galleries, loaders, neon signs, rain, a basketball)
- Homepage JavaScript: ~50KB gzipped, GSAP included
- Hosting: $5/month, deployed by
git push - 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."