Took my portfolio site from a private dev instance to genuinely launch-ready: polish, a real app showcase, accessibility and privacy hardening, a settled hosting strategy. In the process it found and closed the one real public exposure of my home network, with a clean post-mortem on a debugging false alarm.
The challenge
My portfolio site already existed as a vanilla static site (HTML/CSS/JS, no build step, no dependencies), but it ran only as a private dev instance behind a VPN: no public URL, no production home, a decision deferred for months.
It also was not launch-ready: a placeholder headshot, ~15MB of unoptimized images, AI-tell copy, a broken Projects dropdown, no showcase of the apps I'd actually built, and accessibility and privacy gaps.
Running in parallel was a real liability. A public-facing server was still exposed to the internet through a public tunnel and the DNS records behind it: a standing attack surface that had been serving nothing since the app behind it was retired months earlier.
Approach
The organizing principle: keep the front-end dumb and portable, and close the real exposure, not the scary-looking one.
- Keep the front-end a zero-build static site. That portability made the eventual hosting decision cheap and reversible.
- Front-end to a managed CDN; backends deferred per app. I chose a managed static-site host (a reliable global CDN over a home machine on a residential connection, free managed form handling, zero home attack surface) and deliberately decoupled front-end hosting from per-app backend hosting, deferring each backend decision until an app actually goes public.
- A three-tier model as the frame: PRIVATE (VPN-only) → SHARED (a node shared over the VPN with a small, trusted group) → PUBLIC (managed CDN + managed database). Public hosting keeps the home network private while staying scriptable.
- Vendor the flagship demo instead of proxying it. A self-contained, client-side-encrypted demo app (no backend) was vendored as a live demo that ships with the static site and needs no server.
- Honesty-first copy: reframe to neither over- nor undersell the "builds on the side" persona.
Results
Website:
- Images ~15MB → ~2.3MB (~85% reduction) via
sips; originals kept out of the repo. - Fixed a dropdown hover-gap bug (the menu vanished across a 14px gap) with a transparent
::before "bridge" so :hover stays alive. Also caught a flexbox bug squishing the round headshot into an oval (flex-shrink: 0). - Removed ~50 em dashes site-wide (they read as AI-authored) plus a full copy pass and reframe.
- An app showcase with per-app screenshot galleries and a lightbox, the shots auto-generated via an automated screenshot pipeline, plus a fully live vendored demo of the encrypted app.
- Sensitive-data vetting: audited every screenshot; all were test/sandbox data; a real third-party name was cropped out of a stats screen before publish.
- Accessibility + privacy pass: WCAG-AA contrast,
:focus-visible ring, prefers-reduced-motion, and self-hosted web fonts so visitor IPs no longer leak to a third party. The contact form is correctly flagged inert until deployment.
Home-network security:
- Deleted the public tunnel, the one real external exposure, verifying it answered from the provider's external edge before deletion (empty backend, no data impact).
- Deleted the DNS records tied to that exposure, closing off the last path to it. Email records were deliberately kept.
- Confirmed genuinely closed via an off-network cellular test, not just a check from inside the LAN.
Honestly scoped: the site was made launch-ready, not launched. The actual deploy (a host account plus a DNS record) was the deliberate handoff step, and the contact form stays inert until then.
What this demonstrates
- A zero-build static site is a strategic asset: it makes "where does prod live" a cheap, reversible choice, and decouples front-end hosting from unresolved per-app backend questions.
- Never trust a public-IP exposure check run from inside the same LAN. After deleting the tunnel, the home IP still showed ports 80/443 open with an HTTP 403; ~1 hour was chased through router settings before the cause emerged: the tests NAT-hairpinned to the router's own admin page, not any server. Verify from off-network, and read the TLS cert to see what's actually answering before chasing a fix.
- Close the real exposure, not the scary-looking one: the tunnel and the published home IP were the actual risk; the "open port" was a phantom.
- Privacy is a product detail: vet every screenshot for real third-party PII before publishing, and self-host fonts so you don't leak visitor IPs.