anthonyugwuja.dev/contactanthonyugwuja.dev/aboutanthonyugwuja.dev/writinganthonyugwuja.dev/workAnthony UgwujaWorkWritingAboutContactThings I’ve built, and whateach one taught me.

Every section is its own route

All writing

2026-11-06 · 2 min read · Engineering

I replaced a single page of anchor links with six routes, and three bugs disappeared without being fixed.

The shape I started with

The site used to be one long page. Hero, services, projects, about, skills, contact, stacked in a column, with a nav that scrolled you to an anchor. It is the default shape for a portfolio and I had never questioned it.

#aboutscrolls, same documentone pagesix routes, six titles
An anchor moves the scroll position. A route changes what the page is.

Three things that pushed me off it

The nav active state was a lie. It only updated when you clicked a link, so scrolling past a section never changed it, and landing on a deep link or reloading the page showed the wrong one.

Every section needed an entrance animation to justify existing, which is how motion turns into decoration. Fade-and-slide-up on six blocks is not choreography, it is a tic.

And one page can only carry one title and one description, so every view shared metadata that described none of them.

What routes fixed for free

Active state is now derived from the current path, so a deep link, a reload and a back button are all correct by construction. There is no scroll position to track and no observer to keep in sync.

Each view carries its own title and description. And motion has exactly one job left: the transition between views.

from scroll positionscrollY → compare offsets → guessdeep linkwrongreloadwrongback buttonwrongfrom the pathusePathname() === hrefdeep linkrightreloadrightback buttonright
Scroll position is a guess that needs maintaining. The path is already the answer.

The cost

The transition is now load-bearing. On one page, a bad scroll animation is a small annoyance; between routes, a hard cut is the most visible flaw the site can have. I have moved work into the motion phase rather than removed it.

One guard made the change safe. Typed routes turn a link to a route that does not exist into a compile error, so the structure cannot rot quietly as I move things around. The first time I enabled it, it rejected my own nav links - they had been plain strings that nothing had ever checked.

cutcarried across
The same navigation, with and without a transition to carry it.