Files
portfolio/docs/plans/2026-05-07-url-driven-section-routing-design.md
T
2026-05-07 11:45:54 +03:00

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: () => void prop with href: 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 and useState
  • Add activeSlug: string prop (passed from page server component)
  • Pass href={/${section.slug}} to each SectionAccordion
  • Keep children slot pattern for RSC content

SidebarNav (widget)

  • Remove IntersectionObserver and scrollToSection
  • 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 for usePathname)

MobileNav (widget)

  • Section items become <Link> that also close the menu on navigate
  • Use usePathname in a useEffect to 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 — correct isActive and href per section given activeSlug
  • SidebarNav: drop IntersectionObserver mock; mock usePathname; assert active link class
  • MobileNav: items become links; assert close-on-navigate via usePathname effect
  • [[...slug]]/page.tsx: no unit tests (pure orchestration of tested components)

No New Dependencies

next/link and next/navigation already present.