sunilband.com
A dual-personality portfolio with a self-writing blog, OAuth guestbook, and a theme toggle that rewrites the entire site's voice.

Overview
Most portfolios are static résumés in a dark theme. I wanted mine to be the demo. The site has two complete personalities — serious mode is a brutalist monochrome engineering portfolio in Inter; fun mode is the same content rendered in Fredoka, rounded corners, a candy palette, and first-person copy with emoji. Both copy variants live in the DOM and CSS decides which is visible, so the swap is a single class toggle with zero hydration flash. Beneath the surface it's a full-stack app: a Payload CMS blog with a daily AI-publishing pipeline, an OAuth-gated guestbook backed by HMAC-signed sessions, a Resend contact form with disposable-email and MX validation, View Transitions between routes, lazy-mounted WebGL decoration, and JSON-LD schema across every route.
Two Sites In One
The theme system swaps far more than colour. Serious mode is Inter, tight tracking, sharp corners, and third-person copy. Fun mode is Fredoka, generous rounding, vibrant gradients, and first-person voice. Both variants render simultaneously — `data-serious` and `data-fun` attributes let CSS pick the visible one, so toggling is a single root class change with no re-render, no network request, and no flash. The hero portrait switches between grayscale and full colour, stat labels rewrite themselves, and the footer tagline shifts tone. Theme transitions use the View Transitions API for a clip-path circle reveal originating at the toggle button.
Self-Writing Blog
The blog runs on Payload CMS 3.x with a separate MongoDB database, a Lexical-based editor, and a custom GitHub Contents API storage adapter — uploads stream straight to a public images repo. A GitHub Actions cron fires daily: it scrapes GitHub Trending, Hacker News, and dev.to for fresh React/Next.js topics, dedupes against every post already in Payload, then asks Gemini to pick the best candidate and write the full article in one schema-validated call. Cover images come from FLUX.1 schnell, get re-uploaded through Payload so the live post only depends on my own CDN. Posts render with a custom Lexical-to-React renderer, react-syntax-highlighter code blocks, ISR caching, an RSS feed, and per-post view tracking via server actions.
Real Auth, Real Mail
The guestbook is a real OAuth flow — GitHub and Google, CSRF-protected state cookies, code exchange, and a 30-day HMAC-SHA256 signed session cookie. No server-side session store. Posting saves a `pending` entry and emails me an approval link with a single-use token; clicking it flips the status to approved and the message goes live. The contact form does the same on the inbound side — Resend transactional email, honeypot bot detection, disposable-domain blocklist, and a live DNS MX-record check before anything is sent. Both flows use React 19 server actions with typed state objects, so there are no API keys on the client and no separate REST endpoints to maintain.
Performance Without Compromise
The site is packed with decoration — WebGL2 water ripples on the hero name, animated glow orbs, peeping characters in fun mode, a Spotify now-playing badge, particle effects — but none of it blocks first paint. Every heavy visual is dynamically imported with `ssr: false` and gated behind an IntersectionObserver, so it only mounts when scrolled into view. Images use the Next.js pipeline with responsive srcsets and lazy loading. The intro animation FLIPs the site name from a full-screen splash into the sticky header without a single layout recalculation. JSON-LD structured data is injected on every meaningful route. The result is a sub-second LCP and a smooth 60fps scroll despite all the visual richness.
What I Learned
- 01
A theme toggle becomes genuinely interesting when it changes the product's *voice*, not just its hue. Designing two tonal variants forced me to rethink content as a first-class theme axis.
- 02
Rendering both copy variants in the DOM and letting CSS choose the visible one completely eliminates the hydration flash that plagues most theme toggles — slightly larger HTML, instant swap, worth it.
- 03
A CMS plus a strict JSON schema plus a frontier LLM is enough to fully automate a blog. The hard parts aren't the model — they're deduplication against existing content, picking topics from live trending signals, and refusing to publish anything the model truncated mid-thought.
- 04
Heavy decoration and strong Core Web Vitals aren't mutually exclusive. The trick is aggressive deferral: if a user can't see it yet, don't load it yet. Dynamic imports and IntersectionObservers make this trivial in Next.js.


















