Portfolio Website
Next.js + anime.js portfolio built using Claude plugins.
Overview
I wanted a portfolio that felt built rather than assembled — no templates, no off-the-shelf component library, nothing that a thousand other developers also shipped this month. The whole site is designed and coded from scratch using Next.js App Router, Tailwind, and anime.js v4, with Claude Code doing a significant portion of the implementation work alongside me.
Most of the interesting decisions weren't about what to build, but how to build it in a way that stays maintainable. Rather than writing prose in MDX files or a CMS, all project and experience content lives in TypeScript data files — strongly typed, colocated, and editable without a build step or a login.
Design System
Everything on the site flows from a single color token system defined in globals.css. The palette is built around a green center — from --color-primary-50 through --color-primary-900 — and every accent, border, and interactive state pulls from that range rather than hardcoding hex values. Switching themes or repainting the whole site means changing one place.
Typography uses three fonts with intentional roles: Ben (a custom display TTF) handles headings and large type, Nunito Sans covers body and UI text, and JetBrains Mono is reserved for code, eyebrows, and timestamps. All three are injected as CSS variables from the root layout and mapped to Tailwind utility classes — font-display, font-sans, font-mono. Ben especially tends to fall apart at small sizes, so it's kept strictly for display use.
Motion & Interactions
All animation runs through anime.js v4, not CSS transitions or Framer Motion. The tradeoff is that anime.js requires a bit more setup, but the payoff is fine-grained control over easing, staggering, and sequence timing that CSS can't cleanly express. Scroll-triggered reveals use IntersectionObserver to fire staggered entrance animations as elements enter the viewport, keeping the initial page load fast.
The reading progress bar at the top of long pages uses Motion's useScroll and useSpring hooks — a spring-linked animation that follows scroll position with a small amount of lag, which makes it feel physical rather than mechanical. The hero scene has falling leaves that drift across the background using CSS custom properties set per-element for position, drift distance, spin angle, and duration — each leaf is animated with different values to avoid the look of a looping sprite.
Content Architecture
Project writeups are stored in content/projects.ts as typed TypeScript objects rather than markdown files. Each project can use either a flat body array of paragraphs or a structured sections tree made up of ProjectSection and ProjectSubsection objects containing ContentBlock entries — paragraphs, pull quotes, bullet lists, inline images, and code blocks. Adding a new content type means adding one union variant to ContentBlock and one rendering case in the page component.
Long project pages automatically get a sticky table of contents on desktop and a collapsible dropdown on mobile. The TOC tracks the active section using IntersectionObserver — each section heading gets a slug ID derived from its text, and the TOC highlights whichever one is currently closest to the top of the viewport. No external library, just about 80 lines in project-toc.tsx.
Code blocks use Shiki for server-side syntax highlighting — it runs at render time in a Server Component and outputs pre-colored HTML, so the client gets zero additional JavaScript for syntax highlighting. The highlighter singleton lives on globalThis to survive Next.js hot-module reloads in development without spinning up a new instance on every save.
Backend & Infrastructure
The contact form is a Next.js server action — no separate API route, no third-party form service, no client-side secrets. The action validates the submission and hands it off to Resend for delivery. Error and success states are handled with useFormState on the client, which means the form stays functional even without JavaScript.
All pages are statically generated at build time. Dynamic project and blog routes use generateStaticParams to pre-render every slug. The sitemap at /sitemap.xml is generated automatically from the projects and blog content arrays, with new entries appearing as soon as a project is added to the data file. Vercel Analytics and Speed Insights are wired in to track Core Web Vitals in production.