Building This Site with Next.js
A look at the tech stack, design decisions, and architecture behind this site.
Why Next.js?
I needed to rebuild from scratch: resurrecting the old Flask + Postgres stack would have eaten more time than a clean cut. I picked Next.js because the App Router already pushes server components, Server Actions cover mutations without a parallel REST layer I have to babysit, and TypeScript runs through the framework examples I was actually reading.
The old site was Flask with a database I operated myself. The new one still had to feel like current web work while staying maintainable as a solo project that hosts case studies, MDX posts, and repo links in one place.
The Tech Stack
Here's what powers this site:
- Next.js with the App Router and TypeScript
- Tailwind CSS v4 for styling (no preprocessors needed)
- shadcn/ui for accessible, composable UI components
- Framer Motion for smooth scroll animations and hover effects
- MDX for blog posts (you're reading one now!)
- Resend for contact form email delivery
Why Tailwind v4?
Tailwind v4 is a significant evolution. It ships as a single package, requires no separate config file for most setups, and targets modern browsers. The new @theme directive and CSS-first configuration approach means less JavaScript config and more native CSS.
@theme inline {
--font-sans: var(--font-inter);
--font-mono: var(--font-jetbrains-mono);
}Design Philosophy
I wanted something between minimal-flat and neon-overload. The dark theme uses a single cyan–violet ramp (CSS variables for CTAs, accents, and links) so the look stays consistent without being distracting. Motion maps to hierarchy: scroll cues and hover states, nothing purely decorative.
Key Design Decisions
- Dark by default - matches the developer aesthetic without feeling generic
- Gradient accents - shared brand tokens create visual hierarchy
- Framer Motion - scroll reveals stay light; easing matches the rest of the chrome
- Clean typography - Inter for body, JetBrains Mono for code
Server Actions Over API Routes
For the contact form, I used a Server Action with useActionState on the client. The browser handles rich validation UX, while validation, rate limits, and Resend calls stay in one server module that Vitest can hit directly.
"use server";
export async function submitContact(
_prevState: ContactState,
formData: FormData
): Promise<ContactState> {
// Validate with zod, rate limit, send via Resend
}Where It Stands
The site now hosts case studies, MDX posts, contact, a small admin inbox, and a /music page with SoundCloud embeds. Prisma and Neon handle optional persistence, Auth.js gates admin access, and the public route tests keep the main pages from drifting during routine changes.