JK
All work

My Portfolio

Personal portfolio built with Next.js 16, Tailwind v4, and MDX case studies.

Next.jsTypeScriptTailwindMDX

Overview

This site is a personal portfolio built to be fast, readable, and easy to maintain. Case studies are written in MDX and rendered server-side. The design is deliberately minimal — no animations, no hero images, no scroll-jacked sections.

Portfolio homepage — hero section and selected work project grid
Portfolio homepage — hero section and selected work project grid

Goals

The site needed to do one thing well: present work clearly. Secondary goals were maintainability (adding a new case study should take minutes, not hours) and performance (no client JavaScript for content that doesn't need it).


Technical Decisions

Next.js 16 App Router

Pages are React Server Components by default. Content routes hit the filesystem and render server-side — no API round-trips, no loading states for static content. Typed routes (typedRoutes: true) catch broken links at compile time.

@next/mdx with dynamic imports

Each case study is an .mdx file under src/content/. The slug page loads the right file via a typed content map:

const contentMap: Record<string, ContentLoader> = {
  crm: () => import('@/content/crm.mdx'),
  lams: () => import('@/content/lams.mdx'),
  // ...
}

dynamicParams = false means unknown slugs 404 at the edge rather than at runtime.

Custom MDX components

Rather than pulling in @tailwindcss/typography, prose elements are mapped to custom components in src/mdx-components.tsx. This keeps the bundle smaller and makes it straightforward to adjust any element's styling without fighting utility overrides.

Tailwind v4

A single @import "tailwindcss" in globals.css. No tailwind.config.js. Configuration moves to CSS @theme blocks when needed, which keeps tooling simpler.

Radix Dialog for mobile nav

The mobile navigation drawer uses Radix UI's Dialog primitive, which handles focus trapping, scroll locking, and ARIA attributes. The visual implementation is a right-sliding panel with an overlay — two CSS transitions, no animation library.

Work page — full project grid with thumbnails, tags, and year badges
Work page — full project grid with thumbnails, tags, and year badges

What Was Left Out

  • No blog (not actively written)
  • No skills progress bars
  • No separate about page
  • No testimonials
  • No analytics until there's traffic worth measuring

Retrospective

The MDX setup with @next/mdx and dynamic imports worked cleanly. The one friction point was pnpm's symlinked node_modules causing a Turbopack panic — fixed with node-linker=hoisted in .npmrc.

Writing the case studies was more valuable than the code. The process of articulating why decisions were made surfaces things that aren't visible in a codebase.