fix: storybook font rendering and shared fonts module #1
@@ -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