This site used to be a plain Vite + React SPA. Moving it to Next.js 15's App Router meant rethinking the folder structure, the rendering model, and a few libraries that assumed a purely client-side world.
Feature-based folders over a flat components/ dir
Instead of one big components folder, everything now lives under src/features/<feature>/{components,data,lib}, with shared primitives in src/components and src/lib. It scales better once a project has more than one concern (portfolio sections, blog, etc.) living in the same codebase.
Server vs client components
Anything using hooks, refs, or browser APIs needs an explicit "use client" directive. Pages and layouts can stay server components by default, which keeps the JS bundle smaller for content that doesn't need interactivity.
The trickiest part wasn't Next.js itself — it was getting React Three Fiber to behave inside a framework that renders on the server first.
React Three Fiber needs to stay client-only
@react-three/fiber's Canvas can't run during server rendering — there's no WebGL context on the server. Any component that mounts a Canvas gets wrapped in a small client-only component using next/dynamic with ssr: false, then imported normally from a server component.
The other surprise was Next.js's App Router substituting its own vendored React build for client components, which meant the React version pinned in package.json wasn't the whole story — the actual React 3D libraries needed to target had to match what Next was really shipping under the hood.
Tailwind v4's cascade layers
Tailwind v4 wraps every utility class in native CSS @layer blocks. Any custom global CSS that isn't explicitly placed inside @layer base ends up unlayered — which means it silently overrides every Tailwind utility touching the same properties, regardless of specificity. A single unlayered reset rule managed to cancel out every padding and margin utility on the page until it was traced back and fixed.



