Phase 5: Database-Driven Navigation
Status: Implemented
Goal
Replace the hardcoded SECTIONS array with navigation data fetched from the database. The main nav renders from section records where nav_visibility = "main", with a module-level cache and listener-based invalidation.
Navigation Data Flow
Backend: GET /sections?nav_visibility=main
↓
Proxy: pages/api/sections/navigation.ts (adds cache headers)
↓
Client: apiClient.sections.navigation()
↓
Cache: useNavSections hook (module-level singleton)
↓
Render: TopNav maps NavSectionItem[] to nav linksNavigation API Proxy
pages/api/sections/navigation.ts proxies GET /sections?nav_visibility=main from the backend and sets Cache-Control: public, max-age=300, stale-while-revalidate=600. The proxy exists so the client never calls the backend directly.
NavSectionItem Type
shared/lib/navigation.ts defines the frontend representation of a navigation section:
interface NavSectionItem {
id: string;
slug: string;
path: string; // "/{slug}"
label: string; // section.title
icon: NavIcon; // mapped from slug (blog→home, about→user, etc.)
}sectionToNavItem() and sectionsToNavItems() convert backend Section objects to NavSectionItem.
getActiveSectionSlug() extracts the first URL segment and matches it against known sections to determine which nav item is active.
useNavSections Hook
hooks/useNavSections.ts implements a module-level singleton cache:
let cachedSections: NavSectionItem[] | null = null;
let fetchPromise: Promise<NavSectionItem[]> | null = null;
let listeners: Array<() => void> = [];Key behaviors:
- Deduplication: Multiple components calling
useNavSections()share a single in-flight request viafetchPromise. - Caching: Once fetched,
cachedSectionsserves all subsequent reads until invalidated. - Error resilience: On fetch failure, returns
cachedSections ?? []without assigning to the cache. This prevents permanently caching an empty array and allows the next call to retry. - Invalidation:
invalidateNavCache()clearscachedSectionsandfetchPromise, then notifies all mounted components vialisteners. Components re-fetch on the next render cycle.
TopNav Changes
- Renders nav links from
useNavSections()instead of a hardcoded array. - Maps
NavSectionItem.icontoreact-icons/hicomponents via aniconMap. useActiveSection()highlights the current section based on the URL path.- Admin users see a context-aware “New” button that links to
/editor?section_id={activeSectionId}.
Frontend Changes
New Files
shared/lib/navigation.ts—NavSectionItemtype, conversion utilities, active section resolutionhooks/useNavSections.ts— singleton cache hook with invalidationhooks/useActiveSection.ts— derives active section from URL
Modified Files
layout/TopNav.tsx— renders from database sectionsshared/lib/api-client.ts— addedsections.navigation()method
Removed
- Hardcoded
SECTIONSarray (previously in navigation config)
Checkpoint
- Navigation renders from database sections
- Visual appearance unchanged from hardcoded version
- Cache prevents redundant API calls
invalidateNavCache()available for Phase 6 section CRUD operations- Error fallback prevents blank navigation