3.0 KiB
3.0 KiB
URL-Driven Section Routing — Design
Date: 2026-05-07
Status: Approved
Goal
Replace the single-page client-state accordion with multi-page URL-driven routing. Each portfolio section gets its own static URL. The sections list remains visible at all times; clicking a section heading navigates to its page.
Route Structure
Delete app/page.tsx. Create app/[[...slug]]/page.tsx (optional catchall).
| URL | Active section |
|---|---|
/ |
sections[0].slug (first section, URL stays /) |
/intro |
intro |
/bio |
bio |
/skills |
skills |
/experience |
experience |
/projects |
projects |
generateStaticParams emits one entry per section plus the root:
[{}, { slug: ['intro'] }, { slug: ['bio'] }, ...]
Component Changes
SectionAccordion (entity)
- Replace
onClick: () => voidprop withhref: string - Inactive state: render
<Link href={href}>instead of<button onClick> - No
'use client'needed (already a server component)
SectionsAccordion (widget)
- Remove
'use client'directive anduseState - Add
activeSlug: stringprop (passed from page server component) - Pass
href={/${section.slug}}to eachSectionAccordion - Keep
childrenslot pattern for RSC content
SidebarNav (widget)
- Remove
IntersectionObserverandscrollToSection - Add
usePathname()hook for active detection - Active rule:
pathname ===/${item.id}`` or(pathname === '/' && item is first) - Items become
<Link href={/${item.id}}>instead of<button onClick> - Keep
'use client'(required forusePathname)
MobileNav (widget)
- Section items become
<Link>that also close the menu on navigate - Use
usePathnamein auseEffectto close menu on route change (replaces manual close-on-click)
Data Flow
[[...slug]]/page.tsx (RSC)
├─ fetch sections[]
├─ activeSlug = params?.slug?.[0] ?? sections[0].slug
├─ notFound() if activeSlug not in sections
├─ SidebarNav items={navItems} ← usePathname for active state
└─ SectionsAccordion sections activeSlug
├─ SectionAccordion href="/" isActive=true → SectionFactory content
├─ SectionAccordion href="/bio" → Link
└─ SectionAccordion href="/skills" → Link
No client state in the section list. SidebarNav remains client-only for usePathname.
Error Handling
- Unknown slug →
notFound()at page level (404 static page) - Empty sections list →
notFound()at page level
Testing
SectionsAccordion: drop interaction (click/activate) tests; replace with prop-driven assertions — correctisActiveandhrefper section givenactiveSlugSidebarNav: dropIntersectionObservermock; mockusePathname; assert active link classMobileNav: items become links; assert close-on-navigate viausePathnameeffect[[...slug]]/page.tsx: no unit tests (pure orchestration of tested components)
No New Dependencies
next/link and next/navigation already present.