My old site lived (still continues to, I'll take it down on 16th July 2026, my birthday) at imalharr.github.io. Vanilla HTML, CSS, a bit of JS. I built it in 2024 and then kept patching it instead of rebuilding it, which meant it aged badly. The design was fine for a 14-year-old but I'm not 14 anymore. So this summer I finally killed it and built something that actually feels like me.
The stack is Next.js 15/16 App Router (running Next.js 16.3.1 with Turbopack), React 19, TypeScript, Tailwind CSS v4 (with @tailwindcss/typography loaded as a plugin in globals.css so my markdown isn't unstyled block text), and Keystatic as the CMS. Keystatic specifically because it commits content directly to GitHub as markdown files under /content—no database, no subscription, completely free. I'm on Vercel's Hobby plan, so the whole thing costs zero dollars to host.
Here's how the directories actually break down:
app/: Next.js pages and dynamic routes like[slug].components/: UI pieces likeHackerText.tsx(the text scrambler) andHeroFlicker.tsx(avatar hover effects).content/: Collection Markdoc files (projects,achievements,blog,poems).lib/content.ts: A custom frontmatter parser and Markdoc loader. Since Next.js dynamic page data compiles statically (SSG), I wrote resolvers here to load Markdoc files directly from disk during build time, pre-rendering every single project and achievement route without client-side waterfalls.
The design system came first. One background color (#070a3c for dark theme, #f2ede2 for light), and a rule that the design is dark-first neobrutalist with staggered theme transitions (--stagger-idx).
The hero took the longest. It has a PCB circuit-trace background with animated signal traces and traveling glow pulses, and a HackerText component on the heading that scrambles on mount. The two-column layout with my photo on the right came later, after I realized the full-width text version felt too sparse.
The weirdest feature is the poem gate. My poems section is locked behind a two-step mechanism: first you tune a frequency slider to a specific value (it means something, I'm not explaining it), then you enter a passphrase. Three wrong attempts and it locks out for the session. The whole thing is a canvas static noise visualizer with a custom slider. It's probably the thing on this site I'm most proud of because it's completely unnecessary and I built it anyway.
The roughest part of the build wasn't the design at all. It was a missing <a tag in a project page file. Nine cascading TypeScript errors, all pointing to different things, none pointing at the real problem. Every time I pasted the fix, the <a silently got dropped somewhere in the Windows clipboard. I eventually gave up and wrote the entire file directly to disk with a cat > heredoc command in the terminal. That's the kind of thing they don't warn you about in web dev courses.
The other good one was the HackerText hydration mismatch. The component was initializing with a random scramble, which meant server and client rendered different text, which React correctly rejected. Fix was to initialize state with the real text and only start scrambling in useEffect. Obvious in hindsight, annoying to debug at midnight.
The site's still evolving. I've got Keystatic image arrays working now, so I can drop photos directly into project and blog detail page layouts. Everything compiles to static HTML at build time, completely deterministic and blisteringly fast.