aayushbharti.in

aayushbharti.in

A full-stack developer portfolio with a multi-format content pipeline, answer engine optimization, real auth, a real database, and analytics that survive ad blockers. One MDX file publishes to four channels — the site you're reading right now.

Type

Web App

Role

Full-stack Developer & Designer

Built

Q3 2024

Updated

Q1 2026

Source

GitHub

Tech Stack

Next.js
React
TypeScript
Tailwind CSS
PostgreSQL
Prisma ORM
Better Auth
MDX
Zustand
Zod
Motion.dev
PostHog
resend
Shadcn UI
01

Why I Built This

Most developer portfolios are brochures. They solve a presentation problem — make me look hireable — and stop there. I wanted to solve a distribution problem instead.

I write one MDX file. The site turns it into a rendered page, a plain Markdown endpoint for LLMs, an RSS feed entry, and a searchable text index. One source, four formats. Press publish, and humans reading Chrome, AI models ingesting plain text, crawlers indexing structured data, and feed readers polling RSS all get fresh content — from the same pipeline, in the same deploy.

Everything else exists to support that pipeline. OAuth so the guestbook works. PostgreSQL so entries persist. Resend so the contact form sends real emails. PostHog so I know what people actually read. None of it is decorative — every dependency earns its place.

02

Architecture Overview

Three layers, deliberate boundaries.

Content layer — MDX files on disk, validated by Zod at build time, wrapped in React cache() for request-level deduplication. Every content route is statically generated — revalidate = false, dynamicParams = false. No runtime data fetching. The filesystem is the database.

Application layer — Next.js App Router with React 19 and the React Compiler. Server actions handle every mutation and return typed result objects instead of throwing — callers own their side effects. No middleware file; routing logic (social redirects, .md rewrites, PostHog proxy) lives entirely in next.config.ts. Environment variables validated at build time via @t3-oss/env-nextjs — empty strings coerce to undefined, missing secrets fail the build before anything ships.

Infrastructure layer — PostgreSQL via Prisma, better-auth for OAuth, Resend for transactional email, PostHog via first-party reverse proxy. All /ingest/* traffic routes through the app domain — no third-party requests, no ad blocker issues. Discord webhooks fire on form submissions and guestbook entries. Every external service fails silently: a Resend outage doesn't break guestbook submission, a Discord timeout doesn't block the contact form.

Client state is scoped to three Zustand stores (sign-in modal, contact drawer, guestbook with optimistic deletes) and a Cmd+K command palette that indexes every page at module load for instant fuzzy search.

page.tsx — Homepage (hero, globe, projects, skills)
guestbook/ — Auth-gated entries
auth/[...all] — better-auth catch-all
md/ — Plain Markdown for LLM consumption
og/ — Dynamic OG image generation
llms.txt/ + llms-full.txt/ — LLM content indices
03

Content Pipeline

Every piece of content starts as an MDX file. gray-matter extracts frontmatter, Zod validates it against typed schemas, reading-time calculates duration, and next-mdx-remote/rsc renders the result with rehype-slug for heading anchors and Shiki for syntax highlighting. Custom MDX components — Callout, Section, Media, FileTree, Steps, CodeBlock, Accordion — extend what Markdown can express without leaving the authoring format.

Single source, four formats:

  1. HTML — the rendered page with custom components and syntax highlighting.
  2. Plain Markdown — JSX stripped via regex, served at /{page}.md URLs through Next.js rewrites. LLMs get clean Markdown without parsing HTML.
  3. RSS 2.0 — full-content feed at /blog/rss.xml with author metadata and category tags from frontmatter.
  4. LLM text indices/llms.txt returns a structured index with links; /llms-full.txt inlines every post and project body into one response.

The table of contents extracts h2/h3 headings, skips code blocks, and generates slugs via github-slugger. It renders as a sidebar that tracks the active heading through IntersectionObserver — scroll down, and the sidebar follows.

04

Answer Engine Optimization

Search is shifting from links to AI-generated answers. If your content can't be parsed by an LLM, it doesn't exist in that world. So every page on this site speaks two languages: rich HTML for humans, clean plaintext for machines.

Structured data

Every page emits JSON-LD via type-safe generators built on schema-dts — Person and WebSite on the root layout, Article on blog posts, CreativeWork on projects, BreadcrumbList on nested routes. The metadata factory sanitizes titles to 60 chars and descriptions to 155 chars at word boundaries. Google bot directives enable max-snippet: -1, max-image-preview: large, and max-video-preview: -1 for rich snippet eligibility.

Machine-readable endpoints

Every content page has a .md counterpart:

Human URLMachine URL
/blog/some-post/blog/some-post.md
/projects/some-project/projects/some-project.md
/about/about.md

These are Next.js rewrites to /api/md/* route handlers that strip JSX via regex — imports, self-closing components, block-level tags — and return text/plain with frontmatter metadata intact. No HTML parsing required. /llms.txt and /llms-full.txt provide bulk access, cached at 1hr client-side and 6hr at the edge.

RSS

The feed at /blog/rss.xml generates standards-compliant RSS 2.0 with full content bodies — not excerpts — plus author metadata, category tags, and post images. Feed readers and AI aggregators that poll RSS get fresh content automatically.

Traditional SEO

  • Dynamic sitemap — per-type priorities (blog 0.7, projects 0.6, static 1.0), published flag filtering, canonical URLs on every page.
  • OG images — dynamically generated at /api/og with type-specific layouts. Twitter cards get separate truncation limits because Twitter's preview parser is stricter than OpenGraph.
  • Security headersX-Frame-Options, X-Content-Type-Options, strict referrer policy, permissions policy disabling camera, mic, and geolocation.
  • Web manifest — dynamic PWA metadata with theme colors and icons. Rewrite maps /manifest.json to /manifest.webmanifest for broad client compatibility.
  • robots.txt — dynamic generation, points to the sitemap, allows all crawlers.
05

Performance Engineering

The most expensive things on this site are GPU-bound: a WebGL globe, a shader background, canvas sparkles, and a carousel with autoplay. None of them run off-screen. Every one is gated by IntersectionObserver — mount once, observe always, animate only when visible.

  • 3D globe (cobe) — useInView with 200px margin. The requestAnimationFrame loop stops entirely when scrolled out. Spring-based pointer drag only calculates while visible.
  • Shader background — speed drops to 0 when out of viewport. The GPU fragment shader stays compiled but does zero work.
  • Canvas sparkles — raw IntersectionObserver (no library) pauses the RAF loop. Canvas stays mounted, draw cycle stops.
  • Embla carouseluseInView with 100px margin controls autoplay. No transitions fire off-screen.

No rogue animation loops burning CPU in the background.

Adaptive rendering

usePerformanceMode() reads three signals — core count (<= 4), viewport width (< 768px), and prefers-reduced-motion — and degrades GPU-heavy components accordingly. The shader disables, the globe simplifies. Debounced resize at 150ms keeps it responsive without thrashing.

Load deferral

Nothing loads until it has to.

  • PostHog — singleton with lazy import(). First user interaction triggers the download.
  • Vercel Analytics / SpeedInsightsnext/dynamic with SSR disabled.
  • Motion — ships only domAnimation via LazyMotion (~15KB vs ~34KB).
  • React Compiler — handles memoization at build time. Zero manual useMemo or useCallback in the entire codebase.

Build-time optimizations

  • inlineCss — eliminates render-blocking stylesheets in production.
  • removeConsole — strips everything except error.
  • optimizePackageImports — tree-shakes lucide-react, date-fns, motion, cobe, embla-carousel-react, and @paper-design/shaders-react.
  • Images — served as AVIF/WebP with 1-year cache TTL.
06

Backend Systems

Authentication runs through better-auth with GitHub and Google OAuth, backed by a Prisma adapter on PostgreSQL. A single catch-all route at /api/auth/[...all] handles every flow. On the client, signIn, signOut, and useSession export with full type inference. PostHog fires auth_signed_in with the provider on completion, and identify() ties analytics sessions to authenticated users.

The guestbook is the most complete end-to-end feature. A visitor signs in through the OAuth modal, writes a message (Zod-validated, 5-100 chars), hits a rate limit check (3 per 5 minutes via an in-memory sliding window), and the server action creates the Prisma record, sends a confirmation email through Resend, fires a Discord webhook, and revalidates the page — all in one server action. Deletion is author-only with a 5-second undo toast. Soft deletes via a published boolean keep the data intact, and onDelete: SetNull preserves entries even if the user account is removed.

The contact form follows the same pattern: Zod validation, rate limiting (3 per 15 minutes per IP), then resend.batch.send() fires two emails in a single API call — owner notification with IP and geolocation from Vercel headers, and a sender thank-you. Discord gets a webhook with a blue embed. Every external call is wrapped so failures never surface to the user.

07

Key Decisions

MDX over a headless CMS

No API latency, no vendor lock-in, no content/code drift. MDX gives React component embedding, git gives versioning, Zod gives schema validation. The tradeoff — non-technical editors can't contribute — is irrelevant for a personal site.

better-auth over NextAuth

Prisma adapter that works with my schema out of the box, a clean plugin system, and full TypeScript inference from server to client. nextCookies() handles App Router edge cases transparently.

Typed results over thrown errors

Server actions return result objects with a success/failure shape instead of throwing. Callers pattern-match and own their response — toasts, form resets, redirects. The type system enforces exhaustive handling. No try/catch chains, no error boundary gymnastics.

08

What I Learned

Content infrastructure outlasts content. The MDX pipeline, AEO endpoints, and metadata factory pay dividends every time I publish. Write the file, and four formats update. That investment has already saved more time than it took to build.

First-party analytics infrastructure isn't optional. The PostHog reverse proxy doubled data collection overnight. Third-party domains get blocked by ad blockers, which means biased samples and phantom drop-offs. Routing through the app domain fixed that completely.

Performance is a design constraint, not polish. IntersectionObserver gating, adaptive rendering, and deferred loading shaped the component architecture from day one. Treating performance as a hard requirement — not a "nice to have" — forced better abstractions across the board.

Auth is the simplest hard problem. The OAuth flow is trivial. Session management across server components and client hooks, cookie handling in App Router, making sign-in feel instant while three redirects happen behind the scenes — that's where the real complexity lives.

Up Next

Nextdemy

A monorepo-powered learning platform with real payments, real auth, and real content delivery

Web App

Command Menu

Search pages, blog posts, projects, and more.