Why I built a personal website in 2026 (and the technical jugaads that kept it free)
In 2026 everyone has a LinkedIn. A website still does something LinkedIn can't: it lets you be a complete person. Here's every technical decision behind this one — the stack, the AI workflow, and the cost-cutting hacks that kept the whole thing at $0/month.
Let me preempt the obvious question first: why build a personal website in 2026? LinkedIn exists. GitHub exists. Twitter exists. You can get a job without one and most people do.
Here's the honest answer. I Graduated in May 2026. I'm a AI engineer at a stealth medtech startup. My actual work — the stuff I'm most proud of — is under NDA. I can't show you the prior authorization system I shipped. I can't describe the agent architecture. I can't link to the HIPAA-compliant infrastructure I helped build.
That constraint is frustrating in the job market. A recruiter looks at my GitHub and sees a sparse contribution graph — not because I haven't been writing code, but because all of it lives in private repos behind a company firewall. LinkedIn shows titles and dates. It doesn't show what I actually think about, what I build for myself, or what I believe about where AI is heading.
A website changes that equation. It's not about personal branding in the hustle-culture sense. It's about having a place where my professional life is complete — research, personal projects, the way I think about engineering problems, even photos from places I've been. A place I control, that can't be algorithmically suppressed, that doesn't require a login to read, that doesn't deprioritize my content because I don't post enough.
That's why. Now let me tell you how I built it.
The stack decisions
Next.js 16 App Router
I was tempted by Astro. It's genuinely excellent for content-heavy personal sites and the zero-JS-by-default philosophy is compelling. But I work in TypeScript and React full-time. Switching frameworks for a side project is friction I didn't want to optimize away — every hour spent learning Astro's component model is an hour not spent on content.
Next.js 16 with App Router gives me Server Components for the parts that don't need state (most of the homepage, all the blog rendering), and client islands for the parts that do (theme toggle, AI chat widget, mobile nav, active nav tracking). The separation is clean and predictable.
The revalidate pattern for ISR was a specific win. My GitHub contribution data is fetched at build time, cached for 1 hour, then regenerated in the background on the next request. Zero cold-start latency for visitors, always-fresh-enough data for me.
// In any Server Component
export const revalidate = 3_600; // 1 hour ISR
// The GitHub fetch happens at build time + every hour after
const data = await fetchGitHubContributions();IBM Carbon Design System — no Tailwind
This will be the most controversial choice to some people. Everyone uses Tailwind in 2026. I didn't.
The reason is specific: Carbon's design language matches what I wanted to signal about myself. I work in healthcare. I've spent years in contexts where clarity and information density matter more than visual novelty. Carbon was built for IBM enterprise products where a surgeon or a data analyst might be reading the same interface — it prioritizes legibility, consistency, and restraint over decoration.
The other reason is more practical: Carbon gives me a complete design system in one set of CSS custom properties. I defined all the tokens once in globals.css — spacing, typography, color, radius, motion — and everything downstream just uses them. No class proliferation, no design inconsistency across components, no "which shade of gray was that" questions.
/* The entire token contract, light and dark, in ~150 lines */
:root {
--color-primary: #0f62fe;
--color-canvas: #ffffff;
--color-ink: #161616;
--color-surface-1: #f4f4f4;
/* ... */
}
[data-theme="dark"] {
--color-canvas: #161616;
--color-ink: #f4f4f4;
/* ... */
}CSS Modules over Tailwind
CSS Modules give me scoped styles without the cognitive overhead of Tailwind's utility classes. Every component has one .module.css file. The component reads naturally because there are no 15-class strings in the JSX. A className={styles.heroHeadline} tells you what the element is, not what it looks like. That distinction matters when you're skimming code at speed.
The downside is more files. I'm fine with more files.
The AI-native development workflow
This entire site was built with Claude Code as the primary development environment. Not "AI assisted." AI-native — Claude Code was the first tool I reached for, not the last.
The most interesting technique was using parallel subagents for component development.
When I had ten components to build in one session, I didn't build them sequentially. I wrote a brief for each component — here are the design tokens, here is the content data, here is the visual reference, write these specific files, run typecheck, report back — and launched them as parallel subagent tasks.
// Simplified shape of a component brief
type ComponentBrief = {
mission: string; // "Build TopNav component"
tokensPath: string; // where the CSS vars live
contentPath: string; // where site data comes from
visualReference: string; // what it should look like
filesToWrite: string[]; // exact output paths
qualityBar: string[]; // "tsc --noEmit must pass"
};Four subagents ran in parallel for the first batch — TopNav, Hero, CtaBanner, Footer — and all four came back clean in about seven minutes wall-clock. The same work sequentially would have taken roughly thirty minutes. The parallelism isn't just about speed; it's about context isolation. Each subagent had a fresh context with exactly the information it needed. No noise from previous components, no stale assumptions.
Where this gets interesting: the pattern exposes your own context window usage. You start making deliberate decisions about what information each task actually needs versus what you're including out of habit. That discipline carries over to how I write prompts for production AI systems too.
The cost jugaads
"Jugaad" is a Hindi word that roughly translates to a clever, resourceful workaround — making something work with what you have. I wanted this site to cost $0/month to run. Here's how.
Vercel Hobby tier for hosting
Vercel's free tier covers everything this site needs: 100 GB bandwidth per month, serverless functions, ISR, automatic HTTPS, preview deploys per branch, Vercel Analytics. The only real constraint is that serverless functions have a 10-second timeout. All my API routes — GitHub, the AI chat — are designed to either stream (bypassing the timeout for progressive output) or complete well under 10 seconds.
Claude Haiku for the AI chat widget — not GPT-4
The AI chat widget lets visitors ask anything about me — my background, projects, what I'm open to. The system prompt is a RAG-lite setup: every file in personal-docs/ is concatenated and injected as context.
I could have used GPT-4o or Claude Opus. Instead I used Claude Haiku with prompt caching. Here's why:
// personal-docs/ is ~4000 tokens of Markdown
// Loaded once at module level, cached on first request
const SYSTEM_PROMPT = fs
.readdirSync(DOCS_DIR)
.filter((f) => f.endsWith(".md"))
.map((f) => fs.readFileSync(path.join(DOCS_DIR, f), "utf-8"))
.join("\n\n---\n\n");
// Prompt caching means the 4000-token context costs ~10x less after the first call
messages: [
{
role: "user",
content: [
{
type: "text",
text: SYSTEM_PROMPT,
cache_control: { type: "ephemeral" }, // <-- this line is the whole trick
},
{ type: "text", text: userMessage },
],
},
];Anthropic's prompt caching stores the cached prefix for five minutes. If the same system prompt appears at the start of a request within that window, the cached tokens cost 90% less than fresh tokens. For a personal site where the system prompt never changes, this is essentially a permanent discount on every API call after the first one per cache window.
Haiku itself is fast and cheap — it costs roughly 1/20th of Opus per token. For a chat widget where someone asks "what are you working on right now," the quality difference is invisible. Save Opus for problems that actually require it.
Cloudinary free tier for photos
The photos page pulls from my Cloudinary account — 25 GB storage and 25 GB monthly bandwidth on the free tier. Next.js's next/image handles resizing and WebP conversion at request time. I defined the remote pattern once in next.config.ts and the CDN handles everything else.
// next.config.ts
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'res.cloudinary.com',
pathname: '/duozencyz/**',
},
],
}The important detail: I don't use Cloudinary's transformation API in the URL (which can get expensive). I let Next.js's image optimization layer handle the resizing. One CDN, not two.
GitHub GraphQL for contributions — ISR not webhooks
The GitHub contribution strip on the homepage is live data, but it's not real-time. It uses a GraphQL query to the GitHub API with a Personal Access Token, cached for one hour via ISR.
// lib/github.ts — simplified
export async function fetchContributions(): Promise<Week[]> {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
body: JSON.stringify({ query: CONTRIBUTIONS_QUERY }),
next: { revalidate: 3600 }, // ISR cache — no webhook, no polling
});
// ...
}GitHub's free tier allows 5000 API requests per hour. My site makes exactly one GraphQL request per hour. I could exhaust that quota by making requests every second, but there's no reason to. My contribution graph doesn't need to be accurate to the minute.
Dark mode without next-themes
I was using next-themes until React 19 started throwing warnings about <script> tags inside React component trees. Rather than pin an older version, I wrote a custom ThemeProvider in about 40 lines.
// The anti-FOUC trick: runs before React hydrates
<Script
id="theme-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `try{var t=localStorage.getItem('theme');if(t==='dark')document.documentElement.setAttribute('data-theme','dark')}catch(e){}`,
}}
/>The beforeInteractive strategy injects this into the server-rendered HTML before any JavaScript executes. By the time React hydrates, the correct theme attribute is already on <html>. No flash of wrong theme. The ThemeProvider itself is just a context that reads/writes localStorage and toggles data-theme on the document element.
What the site is now
Seven pages, ten main components, an AI chat widget, a blog, a photo gallery, a GitHub contribution strip, JSON-LD structured data for Google, a sitemap, a mobile-responsive hamburger nav, social links, dark mode, ISR, analytics.
Build time: under 1.5 seconds. Monthly cost: $0.
The only running cost is API calls to Claude when someone uses the chat widget. I've been running it for a few days and the cost has been measurably zero — free tier covers it for low traffic.
What I'm building next
The blog itself is the next project. I have a research post about HealthAdminBench (the ICLR paper) that I want to write when it's appropriate to discuss. I want to write about the architecture patterns I've used in production clinical AI — the parts that aren't under NDA. There's a piece forming about why I think computer use agents in healthcare are more important than people realize.
There's also PulseScribe — the ambient clinical scribe I built and won a hackathon with. That one deserves its own post.
If any of this is the kind of thing you think about, my Twitter is @bravim_builds. I post when I ship something.
Stack at a glance: Next.js 16 App Router · TypeScript · CSS Modules · IBM Carbon Design System · Claude Haiku with prompt caching · Cloudinary · Vercel Hobby tier. Total monthly cost: $0.
Reply to this post
Pushback, questions, a different take — I read everything and reply to most.