Next.js App Router के साथ SaaS Dashboard को Sidebar Layout में Migrate करना
- engineering
- nextjs
- app-router
- claude-code
TL;DR
- समस्या: Single-page layout जहाँ "कुछ भी ढूंढा नहीं जा सकता" और "Settings जोड़ने की जगह नहीं है" — यह structural limit पर पहुँच गया था
- समाधान: Supabase, Vercel, और Linear जैसे sidebar + multi-page structure में migrate किया (5 pages, लगभग 2 घंटे का काम)
- Techniques: New pages = Next.js App Router में बस directories जोड़ना; sticky multi-layer sidebar (
top-14+h-[calc(100vh-3.5rem)]);usePathnameसे active state detection;<div> + aria-disabledसे Coming Soon items; LP और dashboard के बीच shared i18n dictionary as SSOT- Tradeoffs: Mobile sidebar drawer design, distributed state management, new pages के लिए higher upfront implementation cost (नीचे discuss किया गया)
हर SaaS Sidebar + Multi-Page Layout क्यों उपयोग करता है
Supabase, Vercel, Linear, Notion, Stripe Dashboard — हर बड़ा SaaS product left sidebar + multi-page layout उपयोग करता है। यह दो competing needs का standard solution है: navigation cost minimize करना और future features के लिए structural room छोड़ना।
"सब कुछ यहाँ dump करो" वाला single-page approach केवल एक हद तक काम करता है। जैसे ही तीन अलग-अलग features cross होती हैं, structural cracks दिखने लगती हैं। HeatMapX उस दीवार से टकरा गया।
पृष्ठभूमि: Single-Page Layout की सीमाएँ
काफी समय तक, HeatMapX dashboard पूरी तरह एक single /dashboard page पर था:
- Announcement banner
- Plan usage card (PlanCard)
- Registered sites list
- CLI installation guide (CliOnboarding)
- Getting Started guide
- Various modals (OnboardingModal, UpgradeDialog, AddSiteDialog)
सब कुछ vertically stack था। शुरुआत में ठीक था, लेकिन जैसे-जैसे features बढ़ीं, दो समस्याएँ नज़रअंदाज़ नहीं की जा सकीं: "कोई चीज़ कहाँ है, पता नहीं चलता," और "Settings जोड़ने के लिए कोई clean जगह नहीं है।" Single-page structure एक structural dead end बन गई थी।
Fix: left sidebar + multi-page layout में migrate करना जो Supabase, Vercel, और Linear उपयोग करते हैं।
अंतिम Route Structure
| Route | Content |
|---|---|
/dashboard |
Home (overview: stats + usage + quick actions) |
/dashboard/sites |
Heatmaps list (site list) |
/dashboard/cli |
CLI & Skills (CLI setup + Claude Code Skill guide) |
/dashboard/billing |
Plan & Billing (current usage + full plan comparison table) |
/dashboard/settings |
Settings (language selection, etc.) |
Next.js App Router इसे आसान क्यों बनाता है
App Router के साथ, आपकी directory structure ही आपकी URL structure है। नया page add करने का मतलब है एक folder और page.tsx file बनाना। बस इतना:
src/app/dashboard/
├── layout.tsx ← सभी dashboard/* पर shared (sidebar + header)
├── page.tsx ← /dashboard (Home)
├── sites/
│ └── page.tsx ← /dashboard/sites
├── cli/
│ └── page.tsx ← /dashboard/cli
├── billing/
│ └── page.tsx ← /dashboard/billing
├── settings/
│ └── page.tsx ← /dashboard/settings
└── components/
├── Sidebar.tsx
├── PricingPlans.tsx
├── LanguagePicker.tsx
└── SkillOnboarding.tsx
5 new pages = 5 new files + shared layout.tsx को update करना। Server-side rendering (Server Components), auth checks (requireUser), और i18n (useLocale) सब अपने आप काम करते हैं।
Sticky Header + Sticky Sidebar: दोनों को एक साथ काम करवाना
Layout structure
// dashboard/layout.tsx
<div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-30 h-14 ...">
{/* logo + theme + locale */}
</header>
<div className="flex flex-1">
<Sidebar />
<main className="flex-1">
<div className="mx-auto max-w-6xl px-8 py-8">
{children}
</div>
</main>
</div>
</div>
Sidebar को header के नीचे stick करवाना
sticky top-0 वाला sidebar header के नीचे slide कर जाएगा। Fix: header की height के बराबर offset करें:
// dashboard/components/Sidebar.tsx
<aside className="sticky top-14 hidden h-[calc(100vh-3.5rem)] w-60 shrink-0 flex-col border-r ... sm:flex">
<nav className="flex-1 overflow-y-auto">
{/* menu items */}
</nav>
<div className="border-t">
{/* logout */}
</div>
</aside>
दो चीज़ें सही करनी हैं:
top-14= 3.5rem = 56px (header height से match करता है)h-[calc(100vh-3.5rem)]viewport height से header को घटाता है — इसके बिना, sidebar नीचे overflow करेगा
flex-col + flex-1 overflow-y-auto + नीचे border-t का pattern logout button को sidebar के footer में pin करता है।
💡 सबक 1: Multiple sticky elements stack करते समय,
topvalues को precisely calculate करें। Tailwind काtop-14(56px) औरh-[calc(100vh-3.5rem)]एक matched pair हैं। अगर बाद में header height बदलें, तो दोनों values को sync में update करें।
Sidebar में Active State Detection
Current page के corresponding menu item को highlight करने के लिए usePathname का उपयोग करें:
'use client'
import { usePathname } from 'next/navigation'
const menuItems = [
{ href: '/dashboard', en: 'Home', ja: 'ホーム' },
{ href: '/dashboard/sites', en: 'Heatmaps', ja: 'ヒートマップ一覧' },
// ...
]
export function Sidebar() {
const pathname = usePathname() ?? '/dashboard'
return (
<aside>
{menuItems.map(item => {
// /dashboard के लिए exact match, बाकी सब के लिए prefix match
const isActive = item.href === '/dashboard'
? pathname === '/dashboard'
: pathname.startsWith(item.href)
return (
<Link href={item.href} className={
isActive
? 'bg-slate-100 font-medium text-slate-900'
: 'text-slate-600 hover:bg-slate-50'
}>
...
</Link>
)
})}
</aside>
)
}
/dashboard strict equality (===) उपयोग करता है; बाकी सब startsWith उपयोग करते हैं। इसका मतलब है /dashboard/sites/abc123 जैसा detail page sidebar में "Heatmaps" को highlighted रखता है।
Coming Soon Pattern: अभी न बने Features को Announce करना
हम A/B Testing और Dynamic UI को "coming soon" के रूप में दिखाना चाहते थे — visible, लेकिन clickable नहीं। Solution: <Link> की जगह <div> with aria-disabled उपयोग करें:
const comingSoonItems = [
{ en: 'A/B Testing', ja: 'A/B テスト' },
{ en: 'Dynamic UI', ja: 'ダイナミック UI' },
]
<p className="text-xs font-semibold uppercase tracking-wider text-slate-400">
Coming soon
</p>
{comingSoonItems.map(item => (
<div
aria-disabled="true"
className="flex cursor-not-allowed items-center gap-2.5 rounded-md px-3 py-2 text-sm text-slate-400"
>
<Icon className="text-slate-300" />
<Bi en={item.en} ja={item.ja} />
<span className="ml-auto rounded bg-slate-100 px-1.5 py-0.5 text-[10px] uppercase text-slate-500">
Soon
</span>
</div>
))}
जब कोई feature ship हो जाए, तो उसकी entry comingSoonItems से menuItems में move करें। Layout और styles automatically reuse होते हैं।
💡 सबक 2: अभी न बने features को announce करना expectations set करता है और anticipation बनाता है। Sidebar के bottom पर दो Coming Soon items users को बताते हैं "A/B Testing और Dynamic UI आ रहे हैं" — बिना कोई additional content maintain किए। यह signal roadmap page से ज़्यादा लोगों तक पहुँचता है, क्योंकि यह हर बार app खोलने पर दिखता है।
LP और Dashboard के बीच Pricing Data Share करना: i18n Dictionary as SSOT
Pricing plan information दो जगहों पर दिखती है: /en/pricing पर LP में और /dashboard/billing पर dashboard में। दोनों जगहों पर data define करने का मतलब है price बदलने पर दो updates — और जल्दी या बाद में, एक miss हो जाती है।
Fix: i18n dictionary को Single Source of Truth बनाएं। LP और dashboard दोनों एक ही t(locale).pricing.plans से read करते हैं:
// LP: src/components/marketing/Pricing.tsx
const d = t(locale).pricing
{d.plans.map(plan => (
<article className="dark-theme-styles">...</article>
))}
// Dashboard: src/app/dashboard/components/PricingPlans.tsx
const d = t(dashLocale).pricing
{d.plans.map(plan => (
<article className="light-theme-styles">...</article>
))}
Data structure identical है। केवल styles (dark vs. light) और CTA behavior अलग है (LP login prompt करता है; dashboard सीधे checkout पर जाता है)। Pricing update करना एक file में बदलाव है।
Home Page पर Server Components के साथ Parallel Data Fetching
Home page को कई aggregated values fetch करने की ज़रूरत है:
- User का current plan
- Registered sites की संख्या
- Monthly page views
- Monthly AI analysis usage
- आज का event count (सभी sites में)
Server Component में इन्हें sequentially await करना slow है। Parallelize करने के लिए Promise.all का उपयोग करें:
export default async function DashboardHome() {
const user = await requireUser()
const [plan, siteCount, monthlyPv, aiUsage] = await Promise.all([
getUserPlan(user.id),
getSiteCount(user.id),
getMonthlyPageViews(user.id),
getMonthlyUsage(user.id),
])
// ...
}
यह load time को लगभग 1.5s से घटाकर ~600ms कर देता है। क्योंकि Next.js Server Component render करने से पहले सभी awaits resolve करता है, parallelization को ध्यान में रखकर design करना तुरंत फायदा देता है।
Usage Bars में Color-Coded "Danger" Signal
Progress bar colors usage percentage के आधार पर dynamically बदलते हैं:
function UsageCard({ used, max }) {
const pct = Math.min(100, (used / max) * 100)
const isOver = pct >= 100
const isHigh = pct >= 80
return (
<div className="h-1.5 w-full rounded-full bg-slate-200">
<div
className={
isOver ? 'bg-red-500' :
isHigh ? 'bg-amber-500' :
'bg-orange-500'
}
style={{ width: `${pct}%` }}
/>
</div>
)
}
जैसे-जैसे users अपना quota approach करते हैं, color shift उन्हें upgrade की तरफ nudge करता है। "Normal" color के रूप में orange-500 उपयोग करने से HeatMapX का brand color हमेशा visible रहता है।
Logout को Header से Sidebar Footer में Move करना
शुरुआती implementation में header के top-right corner में logout link था। Sidebar के आने के बाद, यह sidebar के bottom section में move हो गया। कारण:
- Sidebar footer "important but infrequent" actions की standard location है — Slack, Discord, और Notion सब यही करते हैं
- Header global controls (theme, language) और branding पर focused रहता है
- Accidental logout का risk कम होता है (top-right corner एक high-traffic click zone है)
Claude Code Skill Onboarding
HeatMapX एक Claude Code plugin के रूप में publish है, इसलिए /dashboard/cli page पर CLI और Skill दोनों को side by side surface करना था। बड़ी screens पर two-column layout:
// dashboard/cli/page.tsx
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<CliOnboarding /> {/* npm install -g heatmapx */}
<SkillOnboarding /> {/* /plugin install heatmapx-skill@... */}
</div>
SkillOnboarding English और Japanese दोनों में sample commands दिखाता है:
// English example
"Analyze /pricing with HeatMapX and give me CRO ideas."
// Japanese example
"HeatMapX で /pricing を分析して、改善案を提案して。"
इस तरह, CLI-first users और Claude Code Skill users दोनों को एक ही page से अपना onboarding path मिलता है।
Migration Steps
- Sidebar component बनाएँ (5 nav items + 2 Coming Soon items, inline SVG icons)
- layout.tsx को flex layout में refactor करें (header + sidebar + main)
- Sites list को /dashboard/sites में extract करें (page.tsx content को sites/page.tsx में move करें)
- /dashboard को new home screen के रूप में rewrite करें (stats cards + usage bars + quick actions)
- Billing / cli / settings pages जोड़ें (3 new page.tsx files)
- PricingPlans component बनाएँ (LP के साथ shared data, अलग styles)
- SkillOnboarding component बनाएँ (Claude Code Skill guide)
- LanguagePicker component बनाएँ (settings page के लिए radio-style selector)
कुल: 4 new components + 5 new page.tsx files + layout.tsx और root page.tsx में updates = 11 files। एक ही Claude Code session में, लगभग 2 घंटे में किया।
Tradeoffs: Sidebar Layout के नुकसान
इस migration write-up को थोड़ी credibility देने के लिए, यहाँ honest drawbacks हैं।
- Mobile handling:
sm:breakpoint के नीचे, sidebar छिप जाता है और header में hamburger menu चाहिए। लिखते समय यह अधूरा है — mobile पर sidebar simply गायब हो जाता है, जो एक rough interim state है। - Distributed state management: 5 pages में data फैले होने से, "site count" या "current plan" जैसी shared values हर page पर independently fetch होती हैं। Server Components के साथ latency cost कम है, लेकिन client state पर relying करते समय अधिक deliberate approach चाहिए।
- Higher upfront implementation cost: पहले एक page मतलब एक file था। पाँच pages plus shared layout मतलब 11 files। कम features पर, यह clearly overkill है।
- "Home screen पर क्या रखें?" की समस्या: Single-page layout इसे पूरी तरह avoid करता है — बस सब दिखाओ। Overview screen पर क्या belong करता है, यह एक subtler design call है।
- Backward-compatible URL management: हमने
/dashboardको home screen के रूप में काम करते रहने के लिए design किया, इसलिए existing bookmarks टूटे नहीं। लेकिन अगर भविष्य में sub-paths जैसे/dashboard/clireorganize हों, तो 301 redirects ठीक से set up करने होंगे।
हमारी राय: तीन से अधिक distinct features होने के बाद ये costs worth paying हैं। और दूसरी तरफ भी सच है — अगर आपका product अभी early है और features कम हैं, तो single-page layout शायद सही call है।
निष्कर्ष
Next.js App Router multi-page migrations के लिए एक powerful structure देता है — new pages बस new directories हैं। Sticky sidebars, sticky headers के साथ top-14 + h-[calc(100vh-3.5rem)] के ज़रिए coexist करते हैं। Active state एक usePathname check है। Coming Soon items <div> + aria-disabled हैं। और LP और dashboard के बीच single i18n dictionary को SSOT के रूप में share करना pricing update accidents रोकता है।
ये patterns मिलकर "single-page dump" से "Supabase-style scalable multi-tier SaaS UI" तक जाना possible बनाते हैं, new pages के लिए almost zero barrier के साथ। फिर भी, ऊपर के tradeoffs real हैं — इसका मतलब है migrate करने का सही समय वह है जब तीन distinct features cross हों, उससे पहले नहीं।