Rebuilding this site on Next.js 16 and GSAP
Why I moved from one long scrolling page to a proper multi-page site, and what that changed about performance, indexing and how the content gets edited.
The previous version of this site was a single route. Hero, about, skills,
projects, contact: all six sections sat in app/page.tsx, stacked on top of
each other and joined up with anchor links.
It looked fine. It was still the wrong shape.
One route is one page
A scrolling one-pager gives a search engine one URL, one title and one description to work with. Four projects and a handful of posts all collapse into the same document. There is nothing to rank separately, and nothing to link to except an anchor.
Splitting it into real routes means every project and every post gets its own
URL, its own <title>, its own Open Graph card and its own line in the
sitemap. That was the actual reason for the rebuild. The visual refresh came
along for the ride.
Colour lives in exactly one file
The old stylesheet had colour values scattered through it, and the contact email template had about twenty more hardcoded hexes that nothing else knew about. Changing the accent colour meant grepping for it.
Now there is a two layer token system in a single file. A palette layer holds the raw values, a semantic layer maps them onto roles, and components only ever reference the roles:
:root {
--palette-brand-400: oklch(72% 0.13 251);
--accent: var(--palette-brand-400);
--ink-muted: var(--palette-neutral-400);
--line: var(--palette-alpha-08);
}A lint script fails the build on any hex, rgb() or Tailwind palette utility
found anywhere else, so the rule holds on its own rather than depending on me
remembering it. For the places that cannot read CSS custom properties, like the
email template and the generated OG images, a script parses the stylesheet and
writes out a TypeScript mirror with the values flattened to opaque hex.
Animation that degrades
GSAP replaced Framer Motion. The rule I settled on is to never tween a colour literal and tween a custom property instead, which keeps animated states inside the token system:
gsap.to(card, { "--spotlight-opacity": 1, duration: 0.35 });Every timeline checks prefers-reduced-motion and jumps straight to the end
state rather than animating towards it. The content still arrives, it just does
not move on the way in.
What is next
The content now lives in YAML and is read by copy-ink, a library I wrote, so page copy is editable in the browser instead of buried in a component. Posts stay in MDX, because a technical blog wants code blocks and components more than it wants a browser based editor.