Files
portfolio/docs/plans/2026-05-07-url-driven-section-routing-design.md
T

87 lines
3.0 KiB
Markdown
Raw Normal View History

2026-05-07 11:45:54 +03:00
# 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:
```ts
[{}, { 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.