Customization
Adding Pages
How to create new pages, add routes, and integrate them into the sidebar navigation.
How File-Based Routing Works
Next.js uses file-based routing. Every page.tsx file becomes a route. There is no manual router configuration. The folder path determines the URL:
| File Path | URL |
|---|---|
| app/dashboard/reports/page.tsx | /dashboard/reports |
| app/orders/[id]/page.tsx | /orders/123 |
| app/orders/[id]/edit/page.tsx | /orders/123/edit |
| app/page.tsx | / |
Creating a New Dashboard Page
Create a folder at src/app/(dashboard)/dashboard/reports/ and add page.tsx. The page will immediately be available at /dashboard/reports wrapped in the dashboard layout (sidebar + header):
// src/app/(dashboard)/dashboard/reports/page.tsx
import { PageHeader } from '@/components/shared/page-header'
import { GlassCard } from '@/components/shared/glass-card'
export default function ReportsPage() {
return (
<div>
<PageHeader title="Reports" description="View analytics reports" />
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<GlassCard title="Monthly Revenue" hoverable>
<p className="text-sm text-[var(--haze-text-secondary)]">
Revenue breakdown for the current month.
</p>
</GlassCard>
<GlassCard title="User Growth" hoverable>
<p className="text-sm text-[var(--haze-text-secondary)]">
New user signups over time.
</p>
</GlassCard>
</div>
</div>
)
}Dynamic Routes
Use folders wrapped in square brackets for dynamic route segments. The parameter is passed as the params prop (a Promise in Next.js 15+):
// File: src/app/(dashboard)/orders/[id]/page.tsx
// URL: /orders/123
import { PageHeader } from '@/components/shared/page-header'
import { GlassCard } from '@/components/shared/glass-card'
export default async function OrderDetailPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<div>
<PageHeader title={`Order #${id}`} />
<GlassCard>
<p className="text-sm text-[var(--haze-text-secondary)]">
Order details for {id}
</p>
</GlassCard>
</div>
)
}Choosing a Layout
Pages automatically inherit the layout of whatever route group they live in. Place your file inside (dashboard) for sidebar chrome, (auth) for the auth shell, or (marketing) for the public navbar + footer.
Adding to Sidebar Navigation
To show your new page in the sidebar, open src/lib/navigation.ts and add an entry to the appropriate navigation group:
// src/lib/navigation.ts
export const navigation = [
{
label: 'Dashboards',
items: [
{ label: 'Overview', icon: 'LayoutDashboard', to: '/dashboard' },
{ label: 'Analytics', icon: 'BarChart3', to: '/dashboard/analytics' },
// Add your new page here:
{ label: 'Reports', icon: 'FileBarChart', to: '/dashboard/reports' },
],
},
// ... other groups
]Icons come from lucide-react. The sidebar renders all groups and items automatically.
Fetching Data
Server components can fetch() directly. For interactive UIs (search, pagination, filters) use a client component with useEffect or a library like SWR:
'use client'
import { useState, useEffect } from 'react'
export default function OrdersList() {
const [search, setSearch] = useState('')
const [data, setData] = useState<any>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
const params = new URLSearchParams({ page: '1', per_page: '10', search })
fetch(`/api/orders?${params}`)
.then(r => r.json())
.then(d => { setData(d); setLoading(false) })
}, [search])
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search..." />
{loading ? <div>Loading...</div> : (
<div>
{data?.data.map((order: any) => (
<div key={order.id}>{order.orderNumber}</div>
))}
</div>
)}
</div>
)
}The mock API routes at /api/* support query parameters for page, per_page, search, status, sort, and order.
Tip
Prefer server components by default — they ship zero JS to the browser. Only add 'use client' when you need state, effects, or browser-only APIs.
Next Steps
Learn how to customize the look and feel in the Theming guide, or see all available layouts in the Layouts reference.