docs: add URL-driven section routing design
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user