After 29 years of building software - most of it in PHP, all of it allergic to dependencies - I decided my portfolio should behave less like a resume and more like the gear I love: a production console. Rack units. LEDs that flicker on. A VU meter that actually moves.
The result is bryanhamilton.info, and its last PageSpeed run looked like this: 99 Performance, 100 Accessibility, 100 Best Practices, 100 SEO, 104 KB transferred, 8 requests, 558 ms to load - measured in the browser, not in a lab.
No framework. No build step. No npm. Here's how each piece works, and - because that's where the real lessons live - what broke along the way.
1. The rack: HTML that tells the truth
Each project is a "rack unit" card with three things most portfolios hide: a status LED, an honest status word, and zero marketing.
<article class="rack-card">
<span class="rack-led"></span>
<span class="rack-status">RUNS LIVE</span>
<h3><a href="https://github.com/...">dj-card-preview</a></h3>
...
</article>
The LED color is contract law:
-
Green (
RUNS LIVE): there is a URL you can click right now and it works. -
Orange (
DOCUMENTED): the code is real and CI-tested, but the live instance is retired.
When a tool's live deployment broke, I didn't take the card down or leave a green LED glowing next to a dead link. I flipped the LED to orange. A portfolio that lies to you once is a portfolio you stop trusting.
2. LEDs that power on like real gear
Real rack equipment doesn't fade in politely when it loads. It boots. So when the rack scrolls into view, the LEDs flicker alive left-to-right, one time, then settle into a slow pulse:
@keyframes rack-boot {
0% { opacity: .08; filter: brightness(.4); }
38% { opacity: 1; filter: brightness(2.6); }
55% { opacity: .35; filter: brightness(.8); }
100% { opacity: 1; filter: brightness(1); }
}
@media (prefers-reduced-motion: no-preference) {
.rack-boot .rack-card .rack-led {
animation: rack-boot .9s calc(var(--i, 0) * 70ms) both,
rack-pulse 2.2s calc(var(--i, 0) * 70ms + .9s) infinite;
}
}
One IntersectionObserver adds a rack-boot class the first time the rack enters the viewport, sets a --i index on each card, and the stagger is just arithmetic in calc(). The handoff from boot animation to infinite pulse is scheduled by delay, not JS, so there's no race condition to manage.
The part I care about most is the @media (prefers-reduced-motion: no-preference) wrapper. Every animation on the site - this one, the terminal intro, the background drift - is gated the same way. Accessibility isn't a checklist item at the end; it's the container everything else lives inside.
3. A scroll progress bar disguised as a Venn diagram
The page background is a fixed Venn diagram: one circle for engineering, one for the people-facing work. The overlap used to just sit there. Now it fills.
.venn-core {
position: absolute;
width: calc(min(97vw, 1040px) * 0.30);
background: radial-gradient(closest-side,
rgba(0, 210, 255, .17),
rgba(0, 210, 255, .05) 58%, transparent 72%);
opacity: var(--vp, 0);
}
var max = document.documentElement.scrollHeight - window.innerHeight;
var p = Math.min(1, Math.max(0, window.scrollY / max));
venn.style.setProperty('--vp', p.toFixed(3));
Eleven lines of JS inside a requestAnimationFrame-throttled, passive scroll listener. As you descend the page, the intersection of the two circles glows brighter until, at the footer, the overlap is fully lit. The site's entire thesis - the engineer who can also talk to humans - is rendered as an opacity value.
That's the trick I'd hand to anyone building a personal site: if your brand has a central metaphor, make the mechanics of the page narrate it. Nobody consciously notices a scroll progress bar inside a Venn diagram. Everybody feels it.
4. The equalizer that actually listens
The signature piece is an 8-bar equalizer that reacts to real audio - not a CSS keyframe fake. When you hover a demo card, it plays a 30-second track preview through the Web Audio API and drives eight canvas bars from the analyser:
const ctx = new AudioContext();
const src = ctx.createMediaElementSource(audio);
const analyser = ctx.createAnalyser();
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.8;
src.connect(analyser);
analyser.connect(ctx.destination);
// 8 bars, sampling every other bin of the low half
function draw() {
analyser.getByteFrequencyData(dataArray);
for (let i = 0; i < 8; i++) {
const v = dataArray[i * 2] / 255;
bars[i].style.height = Math.max(4, v * 80) + 'px';
}
requestAnimationFrame(draw);
}
Two lessons worth the tuition:
- Autoplay policy is a feature, not a bug. Browsers won't let you play audio until the user interacts. The demo shows a one-time "click anywhere to activate" gate. Fighting this is a losing game; designing around it is a UX opportunity.
-
A CSS "fake" EQ is detectable in about half a second. If the bars don't move with the kick drum, people feel the lie immediately.
fftSize: 64withsmoothingTimeConstant: 0.8gives you chunky, musical bars instead of noise.
5. The search index I'm proudest of
The site has instant search with keyboard navigation and ARIA combobox semantics, built on a prebuilt JSON index and a tiny client-side scoring function. No server round-trips, no search service. At build time the content gets flattened into one JSON file; at runtime, ~60 lines of JS score and match against it.
I extracted the whole pattern into a standalone repo (instant-search-index) because it's the most reusable thing I've built in years: prebuilt index + scored matching + proper combobox ARIA roles, and it works in anything.
6. What broke (and what it taught me)
No tutorial-style post is honest without the blood.
The 16 KB wall. My host's FTP silently truncates uploads at exactly 16,384 bytes. My resume-builder PHP file is 24,084 bytes. The deploy "succeeded" and produced a corrupt PDF. Diagnosis took byte-counting and MD5 comparisons; the fix was a chunked-upload kit (split the file, base64 the parts, assemble server-side, verify MD5 before writing). The real lesson: when a deploy fails weirdly, compare bytes, not screenshots.
The cache header that ate my deploys. I shipped cache-control: max-age=2592000, immutable on my HTML. That header is meant for fingerprinted assets like app.abc123.js - not for a homepage I edit daily. Result: my browser happily served me a week-old page while I debugged code that was already live. I only proved the code worked by loading the site in headless Chrome, which had no cache. The fix is one .htaccess block:
<FilesMatch "\.html$">
Header set Cache-Control "public, max-age=0, must-revalidate"
</FilesMatch>
If you take one operational lesson from this post: immutable is for hashed assets. HTML gets max-age=0, must-revalidate. Write that on a sticky note before it costs you an afternoon.
The half-leading PDF. My PDF generator advanced the Y cursor by 48% of the line height, stacking lines on top of each other. I didn't see it for weeks because a separate bug (a missing %PDF-1.4 header) made the file unopenable, masking the layout bug underneath. Fix one bug, meet its friends.
7. Why zero dependencies, seriously
I've been doing this since before frameworks were the default, so take this with the appropriate salt. But the math on this site:
- 104 KB total. The median framework starter ships more than that before it renders anything.
- 8 requests. No font carousel, no analytics tag parade.
- 99-100 across every Lighthouse category - including the one most portfolios fail on purpose: Accessibility, because animations respect reduced-motion and the search is a real ARIA combobox.
Zero-dependency isn't nostalgia. It's a debugging philosophy: when everything in the stack is code you wrote, nothing is someone else's mystery. When the PDF breaks, I can read every line that produced it.
Come kick the tires
The site is live at bryanhamilton.info - scroll slowly, let the rack boot, watch the overlap glow as you reach the bottom. The code is on GitHub, ten repos deep, each one CI-badged and extracted from production code rather than written for show.
And if you build something with any of these patterns - the LED boot, the analyser EQ, the instant search - tell me in the comments. I want to see the racks other people bolt together.
Top comments (1)
All ten repos behind the rack are at github.com/bryanhamiltondev — each one CI-badged and extracted from production. Happy to go deeper on any pattern in the article.