Personal portfolio
Built as a Turborepo monorepo with a Next.js 16 frontend and Sanity v5 CMS.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 6 |
| Styling | Tailwind CSS v4 |
| CMS | Sanity v5 (headless) |
| AI Chatbot | Claude API (Haiku) via @anthropic-ai/sdk |
| Resend | |
| Validation | Zod v4 |
| CAPTCHA | Cloudflare Turnstile |
| Analytics | Vercel Analytics, Speed Insights, GA4, Umami |
| Linting | oxlint |
| Formatting | oxfmt |
| Monorepo | Turborepo + pnpm workspaces |
apps/
web/ → Next.js portfolio site
studio/ → Sanity Studio CMS
packages/
config-typescript/ → Shared TypeScript configs
sanity-schemas/ → Shared Sanity schema definitions
- Node.js >= 20
- pnpm >= 10
pnpm installEach app has its own .env file (per Turborepo best practice — no root .env):
# Web app (Next.js) — all env vars
cp apps/web/.env.example apps/web/.env.local
# Studio (Sanity) — only project ID + dataset
cp apps/studio/.env.example apps/studio/.envSee apps/web/.env.example for all available variables. The site works without any env vars — it uses hardcoded fallback data when Sanity is not configured.
pnpm devThis starts the Next.js dev server (web) and Sanity Studio (studio) concurrently via Turborepo.
pnpm buildpnpm lint # Check with oxlint
pnpm lint:fix # Auto-fix lint issues
pnpm format # Check formatting with oxfmt
pnpm format:fix # Auto-fix formattingpnpm type-checkFeatures can be toggled via environment variables. All default to disabled.
| Flag | Controls |
|---|---|
NEXT_PUBLIC_ENABLE_CHATBOT |
AI chatbot (requires ANTHROPIC_API_KEY) |
NEXT_PUBLIC_ENABLE_BLOG |
Blog pages (requires Sanity content) |
NEXT_PUBLIC_ENABLE_CONTACT |
Contact form (requires Resend + Turnstile) |
When a feature is disabled:
- Pages return 404
- API routes return 404
- Nav links and homepage sections are hidden
- Sitemap excludes disabled routes
The site gracefully degrades when Sanity is not configured:
- Profile data falls back to
FALLBACK_PROFILEinlib/sanity.ts - Project/blog fetches return empty arrays
- No runtime errors — the site renders fully with static data
- CSP headers — Content Security Policy configured in
next.config.ts - Security headers — HSTS, X-Frame-Options, Permissions-Policy, etc.
- API rate limiting —
proxy.tsenforces per-IP limits (10 req/min chat, 5 req/min contact) - Cloudflare integration — Reads
cf-connecting-ipfor real client IP - Turnstile CAPTCHA — Protects contact form from bots
- Honeypot field — Secondary bot detection on contact form
- Prompt injection guards — Regex patterns block common injection attempts on the chat API
- Input validation — Zod schemas on all API inputs
- Vercel Analytics — Page views and web vitals
- Vercel Speed Insights — Core Web Vitals monitoring
- Google Analytics 4 — Via
@next/third-parties(optional) - Umami — Privacy-first analytics, no cookies (optional)
- Structured logging — JSON logs in production via
lib/logger.ts
Designed for Vercel. Connect the repo and set environment variables in the Vercel dashboard.
Recommended: Put Cloudflare in front of Vercel for DDoS protection, WAF rules, and edge rate limiting at no cost.
Set no env vars — the site deploys with fallback data, all optional features disabled.
Set all env vars from .env.example and enable features as needed.
Private. All rights reserved.