I didn't want another template site
I had a website. It was fine. Purple and gold gradients, cinema marquee animations, a vault page nobody visited, an admin dashboard I built for fun and never used. It looked like what happens when a developer discovers framer-motion and doesn't stop.
So I started over. Not with a design tool. Not with a template marketplace. I opened Claude Code and said: "What's out there right now for personal brand websites? What should mine look like?"
Session one: figure out the brand, then build it
The first conversation was about positioning, not code. I run an AI automation agency. I'm finishing my MBA at MIT Sloan. I make short-form video. The old site tried to say all of that and ended up saying none of it.
Claude and I worked through the brand brief together. The one-liner landed on "I build AI workflows that replace manual work with systems." Dark background, burgundy accent, three fonts (serif for headlines, sans for body, mono for labels and code). The vibe: tech-editorial. Like a developer's portfolio crossed with a magazine.
Once the brief was locked, I wrote a PRD. Five pages: homepage, about, blog, services, book a call. Three service tiers. One funnel. Social content brings people in. Blog posts build trust. The site converts.
Then I went looking for components. 21st.dev had exactly what I needed. An infinite grid with a cursor-reactive reveal for the hero background. A file tree component to show my content pipeline architecture. Glassmorphism cards for pricing. I pulled three components and built the visual language around them.
The actual build was one commit. 60+ files changed. 2,300 lines of old code deleted. Every old component gone: the gradient mesh, the tilt cards, the custom cursor, the easter eggs. Every old page gone: the vault, the admin dashboard, the auth callback. All 16 old blog posts wiped.
Fresh start. The PRD said what to build. Claude built it.
What the site runs on
Next.js 16 with React 19, TypeScript, Tailwind CSS v4. Deployed on Vercel. No framer-motion. That was a locked decision from the PRD because framer-motion 12.x hangs with React 19. Even a simple <motion.h1> causes an infinite loading state. So I went pure CSS: IntersectionObserver for scroll animations, backdrop-blur for glassmorphism, CSS transitions for everything else.
The design system lives in a single Tailwind @theme block in globals.css. Every color, font, and spacing token defined once. Dark mode only. Film grain overlay at 0.03 opacity for texture.
| Token | Value |
|---|---|
| Background | #0C0C0E |
| Card | #1A1A1E |
| Accent (burgundy) | #C43B4F |
| Terminal green | #4ADE80 |
| Serif | Fraunces 700 |
| Sans | Plus Jakarta Sans |
| Mono | JetBrains Mono |
Session two: polish and infrastructure
The second session was smaller. Headshot quality (three commits going back and forth on crop and watermark removal). Button visibility on the secondary CTA. And then the big one: email-gated blog posts.
I wanted blog posts that double as lead magnets. Reader sees the first half, hits an email gate, enters their address, and the rest of the post appears. The email goes to both Supabase (for my database) and Kit/ConvertKit (for email sequences) with a lead-magnet:[slug] tag so I can segment by which post converted them.
The EmailGate component is a three-phase state machine: locked (form visible), success ("Sweet! Continue reading." in terminal green), revealed (gated content fades in). Each transition is a 400ms crossfade. localStorage remembers the reader so they never see the gate twice.
The section below covers the exact build decisions, what broke, and the full component inventory.
Keep reading
Enter your email to see the rest of this post.