Personal Portfolio Site

A modern, content-focused portfolio built with Next.js 16, React 19, and TypeScript. Built from scratch with AI-assisted development.

Role: Developer & DesignerDec 2025

For years I told myself I'd build a personal site. The domain collected dust. The idea never left the notes app.

Then I stopped planning and started building. Two days later, this site was live.

What Changed

I used to think shipping a site meant weeks of work. Picking fonts. Debugging layouts. Writing copy that never felt right. This time I had a different approach: build with AI, ship fast, improve later.

The result: 51 commits in 48 hours. A blog with audio narration. A contact form that actually works. A design system I can extend. Not perfect, but real.

What surprised me wasn't the speed. It was how the work felt. Less searching, more building. Less second-guessing, more shipping. The tools handled the tedious parts so I could focus on what mattered.

This page is the full story. How it came together, what's under the hood, and what I'd do differently next time.

Cursor IDE
Cursor IDE with Claude assistant

Tech Stack

CategoryTechnologyVersionPurpose
FrameworkNext.js (App Router)16.0.10Server components, routing, SSR
UI LibraryReact19.2.3Component architecture
LanguageTypeScript5.xType safety
StylingTailwind CSS4.xUtility-first styling
Componentsshadcn/uiNew YorkPre-built accessible components
Contentnext-mdx-remote5.0.0MDX rendering with frontmatter
Parsinggray-matter4.0.3Frontmatter extraction
IconsLucide React0.561.0SVG icon library
EmailSendGrid8.1.6Contact form delivery
AnalyticsGoogle Analytics 4Traffic and behavior tracking
AudioElevenLabs Audio NativeText-to-speech for blog posts
Dependencies
Package.json dependencies

Architecture

The application follows Next.js App Router conventions with clear separation between pages, components, content, and utilities.

ftchvs_26/
├── app/                    # Next.js App Router pages
│   ├── layout.tsx          # Root layout with fonts and nav
│   ├── page.tsx            # Home page
│   ├── globals.css         # Global styles and design tokens
│   ├── about/              # About page
│   ├── blog/               # Blog listing and [slug] pages
│   ├── projects/           # Projects listing and [slug] pages
│   ├── contact/            # Contact form with API route
│   └── resume/             # Resume page
├── components/             # React components
│   ├── nav.tsx             # Navigation with mobile sheet
│   ├── theme-toggle.tsx    # Dark/light mode toggle
│   ├── tts-player.tsx      # ElevenLabs audio player
│   ├── mdx-content.tsx     # MDX renderer
│   └── ui/                 # shadcn/ui components
├── content/                # MDX content files
│   ├── blog/               # Blog posts
│   ├── pages/              # Static pages (about)
│   └── projects/           # Project case studies
├── lib/                    # Utilities
│   ├── content.ts          # Content loading and parsing
│   ├── constants.ts        # Site configuration
│   ├── schema.tsx          # JSON-LD structured data
│   └── utils.ts            # Helper functions
└── public/                 # Static assets

Data Flow

Content flows from MDX files through lib/content.ts to pages:

  1. MDX files store content with YAML frontmatter
  2. gray-matter parses frontmatter into typed objects
  3. next-mdx-remote serializes MDX for React rendering
  4. Page components fetch content at request time
  5. MDX components render the final HTML
GitHub Repo
GitHub repository structure

Typography Design System

I built a token-based typography system for consistent styling across all pages. Each token maps to specific Tailwind classes.

TokenCSS ClassStylesUsage
Page Title.text-page-titletext-xl font-medium mb-6H1 headings
Section Title.text-section-titletext-lg font-medium mt-8 mb-4H2 headings
Subsection.text-subsection-titletext-base font-medium mt-6 mb-3H3 headings
Body.text-bodytext-[15px] leading-relaxed text-foreground/90Paragraphs
Body Container.text-body-containerspace-y-4 text-[15px] leading-relaxedMultiple paragraphs
Meta.text-metatext-[14px] text-muted-foregroundDates, tags, labels
Link.text-linkunderline underline-offset-2 hover:text-foregroundInline links
List Item.text-list-itemtext-sm text-foreground/80Card and list text

Font Stack

  • Geist Sans — Primary typeface for body text and UI
  • Geist Mono — Code blocks and the terminal logo

Both fonts load via next/font/google with display: swap for optimal performance.

Color System

Colors use the OKLCH color space for perceptually uniform adjustments. The system includes:

  • Light mode — Neutral grayscale with subtle accents
  • Dark mode — Deep backgrounds with high contrast text
  • Semantic colors — Primary, secondary, muted, accent, destructive, success

Key Features

Dark Mode

Theme preference persists in localStorage and respects system settings on first visit. A blocking script in <head> prevents flash of wrong theme.

<script dangerouslySetInnerHTML={{
  __html: `(function(){
    var t = localStorage.getItem('theme') ||
      (matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light');
    if (t === 'dark') document.documentElement.classList.add('dark')
  })();`,
}} />

Text-to-Speech

The player loads dynamically to avoid hydration issues:

const TTSPlayer = dynamic(
  () => import('@/components/tts-player').then((mod) => mod.TTSPlayer),
  { ssr: false }
);

Creating a Voice Clone on ElevenLabs

To create your own voice clone for the audio player:

  1. Sign up or log in to ElevenLabs
  2. Navigate to the Voices section in the dashboard (left sidebar)
  3. Click Add a new voice
  4. Choose Professional Voice Clone (recommended for best quality) or Instant Voice Clone (faster, requires less audio)
  5. Upload audio samples:
    • Professional Voice Clone: At least 1 hour of high-quality audio (ideally 2-3 hours for best results)
    • Instant Voice Clone: Minimum 30 seconds of clean audio
    • Split long recordings into ~30-minute samples for easier uploading
    • Use clean, high-quality recordings without background noise, echo, or unwanted sounds
  6. Optionally record directly in the interface using Record yourself with provided sample scripts
  7. Name and label your voice clone
  8. Confirm you have the right and consent to clone the voice
  9. Click Save voice to train the model
  10. Once trained, copy the Voice ID from the voice settings
  11. Configure it in the project by updating ELEVENLABS_VOICE_ID in lib/constants.ts

Voice Sample

Here's a sample of the cloned voice:

Voice Sample
0:00 / 0:00

To generate the audio sample, you can use the ElevenLabs Text to Speech playground. Select your cloned voice, enter the text, click Generate, then download and save the file as voice-sample.mp3 in the public/audio/ directory.

Blog Audio Narration (local Kokoro)

Update — April 2026. Blog posts and the About page now use Kokoro-82M, an Apache-2.0-licensed TTS model (82M parameters, voice af_heart) running locally on CPU via kokoro-onnx. Generation is deterministic, free, and has no external API dependency. The voice sample above is kept as the original ElevenLabs-cloned voice — only the blog narration was migrated.

Setup is one-time: npm run setup:kokoro creates a gitignored .kokoro/ directory with a Python venv and the ONNX model (~340 MB). After that, npm run generate-audio narrates all English content and updates public/audio/manifest.json with per-post content hashes. On an Apple M4 Max the model runs at roughly 5× real-time — a three-minute post narrates in about thirty seconds.

Contact Form

The contact form uses SendGrid for email delivery with:

  • Client-side validation
  • Server-side API route at /contact/api
  • Reply-to header for direct responses
  • Error handling with user-friendly messages

SEO

Every page includes:

  • Metadata — Title, description, keywords
  • Open Graph — Social sharing images and text
  • Twitter Cards — Twitter-specific formatting
  • JSON-LD — Structured data for Person, Website, Article, Breadcrumb
  • Sitemap — Auto-generated at /sitemap.xml
  • Robots — Search engine crawl rules
Vercel Dashboard
Vercel dashboard

Build Process

Phase 1: Project Setup

Started with create-next-app using the App Router template. Added TypeScript, Tailwind CSS 4, and configured shadcn/ui with the New York style variant.

npx create-next-app@latest ftchvs_26 --typescript --tailwind --app
npx shadcn@latest init

Phase 2: Core Layout

Built the root layout with Geist fonts and a responsive navigation. The nav includes a desktop menu and a mobile sheet (slide-out drawer) from shadcn/ui.

The terminal logo (>_) uses a shimmer animation that adapts to light and dark modes.

Phase 3: Content System

Created lib/content.ts to handle MDX loading:

  • getAllPosts() — Fetch all blog posts sorted by date
  • getPostBySlug() — Fetch single post with serialized MDX
  • getAllProjects() — Fetch all projects
  • getProjectBySlug() — Fetch single project with serialized MDX
  • getPageBySlug() — Fetch static pages like About

Each function reads from the filesystem, parses frontmatter with gray-matter, and serializes content with next-mdx-remote.

Phase 4: Pages

Built six main routes:

  • Home — Introduction with recent writing list
  • About — Professional background loaded from MDX
  • Blog — Post listing and individual post pages
  • Projects — Project showcase (this page)
  • Resume — Professional experience and skills
  • Contact — Form with SendGrid integration

Phase 5: Integrations

SendGrid: Configured API route to send emails from the contact form. The sender must be verified in SendGrid's authentication settings.

Google Analytics 4: Added via @next/third-parties with conditional loading based on NEXT_PUBLIC_GA_ID.

Phase 6: Design System

Implemented typography design tokens in globals.css. Added OKLCH color variables for light and dark modes. Created shimmer animation with CSS custom properties.

Phase 7: SEO

Added metadata to every page. Created sitemap and robots.txt generators. Implemented JSON-LD structured data for Person, WebSite, Article, and BreadcrumbList schemas.

Phase 8: Deployment

Pushed to GitHub and imported into Vercel. Configured environment variables:

  • NEXT_PUBLIC_SITE_URL — Canonical URL
  • NEXT_PUBLIC_GA_ID — Google Analytics
  • SENDGRID_API_KEY — Email delivery
  • CONTACT_EMAIL_TO — Recipient address
  • CONTACT_EMAIL_FROM — Verified sender
Vercel Deployment
Vercel deployment details

Changelog

A detailed timeline of development, extracted from the Git commit history. Latest first.

Development Stats

  • Total commits: 51
  • Development time: ~2 days
  • Major features: 8
  • Bug fixes: 12
  • Refactors: 15
  • Style updates: 16

December 13, 2025 — Day 2: Polish & Features

Night: Final Polish

CommitDescription
20107f4Major: Comprehensive SEO improvements
e3ec3c2Simplified about page intro
c21eb7bCentralized voice ID configuration
9311bc6Configured custom ElevenLabs voice
f8533bfMajor: Optimized ElevenLabs to reduce API costs
f8d31cfPositioned TTS player above summary
7da002aUpdated blog list typography
872a6baRevised blog post for clarity and storytelling

Evening: Audio Optimization

CommitDescription
fc9cea4Used ref to insert HTML (hydration fix)
fb9303fUsed iframe embed for Audio Native
7384478Restored Suspense wrapper for MDX compatibility
ff9ca71Major: Audio Native API with full content projects
76e6e08Removed Suspense for SSR'd Audio Native
36c512cLoad Audio Native via useEffect
5073332Hardcoded public user ID
ea4e5c7Used next/script for Audio Native loading
8a5ee20Simplified to Audio Native integration
32fa6caAdded script to clear audio cache
61382caAdded audio caching to reduce API calls
0ab2855Pre-generate TTS audio on page load
d59988fAdded debug logging to TTS player

Late Afternoon: Audio Integration Sprint

CommitDescription
d018e8eRemoved ElevenLabs footer from player
156eda5Made speed selector more minimalist
b87be4fRemoved ElevenLabs header from player
1d6af2fShow full audio player UI immediately
1a90ebfUpgraded TTS player with ElevenLabs UI components
738ebb4Replaced Audio Native with TTS API
fa6fd42Added Cursor IDE configuration and rules
5152aecWrapped MDXRemote with Suspense for React 19
c7b54b2Major: Added ElevenLabs Audio Native TTS integration

Afternoon: Typography & Resume

CommitDescription
2ff75d4Updated learning statement in blog post
0c9b17bUpdated resume tagline
3e6b185Added dash prefix to Skills and Education items
3371a03Removed "Looking For" section from resume
649010dImproved resume typography and alignment
f03743eUpdated font sizes for list items, links, meta text

Late Morning: Content Strategy

CommitDescription
c5d35e4Added audience targeting to About page
45e61a2Toned down "building tools myself" messaging
b7a8cd8README updates for new positioning
ae9f3a3Refined growth marketer positioning across all pages
701a36fRepositioned site as "Growth Marketer who codes"
db41419Replaced projects page with "Coming Soon" placeholder

Morning: Design System

CommitDescription
4b7f5ffUpdated README with typography tokens, SendGrid, GA4 docs
d850b7eReordered nav menu: About, Blog, Projects, Resume, Contact
a17536bMajor: Typography design token system implementation
a91398fUpdated favicon to terminal prompt icon (>_)
c85a8c8Removed focus ring border on terminal logo
af50e11Refactored shimmer animation for light/dark mode support

December 12, 2025 — Day 1: Foundation

CommitDescription
0b4e0a2Major: Integrated SendGrid for contact form with enhanced UI
5bf8d6aFixed text selection on terminal logo click
2ab20b1Disabled static generation for MDX (React 19 compatibility fix)
0d1a813Added pages, components, and core features
891648cMerged README documentation
08bc5e0Major: Initial Next.js portfolio site with blog and projects structure
7850991Repository initialization on GitHub
20aed18Initial commit from Create Next App

Key Milestones:

  • Complete App Router structure with 6 routes
  • MDX content system with gray-matter parsing
  • shadcn/ui component library integration
  • Contact form with email delivery
  • Responsive navigation with mobile drawer
Commit History
GitHub commit history

Screenshots

Homepage Light
Homepage light mode
Homepage Dark
Homepage dark mode
Mobile Nav
Mobile navigation
Contact Form
Contact form
Blog Post
Blog post with audio player

What I Learned

AI-assisted development is real. Cursor and Claude handled boilerplate, caught errors, and suggested patterns I wouldn't have found alone. The bottleneck shifted from typing to thinking.

TypeScript pays off early. Type errors caught bugs before they hit the browser. The initial setup time saved hours of debugging.

Design tokens scale. Defining typography classes once meant consistent styling without constant decisions. Changes propagate automatically.

MDX is flexible. Frontmatter schemas let content drive the UI. Adding a new field takes minutes, not refactors.


Next Steps

  • Add more projects as case studies
  • Implement blog post search and filtering
  • Add RSS feed for blog subscribers
  • Explore ISR (Incremental Static Regeneration) for content updates

Outcomes

  • Built from scratch with AI-assisted development (Cursor + Claude)
  • Implemented custom typography design token system
  • Integrated ElevenLabs Audio Native for text-to-speech
  • Configured SendGrid for contact form functionality
  • Deployed on Vercel with comprehensive SEO

Links