Routing
Y3NKO uses Next.js 14 App Router for file-system based routing.
Route Structure
app/
├── page.tsx # /
├── layout.tsx # Root layout (wraps all pages)
├── (auth)/ # Route group (no URL impact)
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── accommodations/
│ ├── page.tsx # /accommodations
│ └── [slug]/
│ └── page.tsx # /accommodations/beach-villa
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # /dashboard
│ ├── bookings/
│ │ ├── page.tsx # /dashboard/bookings
│ │ └── [ref]/page.tsx # /dashboard/bookings/Y3K-ABC123
│ └── profile/page.tsx # /dashboard/profile
├── api/
│ ├── bookings/route.ts # /api/bookings
│ └── webhooks/
│ └── paystack/route.ts # /api/webhooks/paystack
└── not-found.tsx # 404 pageRoute Types
Page Routes
A page.tsx file makes a route publicly accessible:
// app/accommodations/page.tsx
export default function AccommodationsPage() {
return <div>Accommodations</div>
}Dynamic Routes
Use brackets for dynamic segments:
// app/accommodations/[slug]/page.tsx
export default function ListingPage({ params }: { params: { slug: string } }) {
return <div>Listing: {params.slug}</div>
}Catch-All Routes
Use [...slug] for catching multiple segments:
// app/docs/[...slug]/page.tsx
// Matches /docs/a, /docs/a/b, /docs/a/b/c
export default function DocsPage({ params }: { params: { slug: string[] } }) {
return <div>Path: {params.slug.join('/')}</div>
}Route Groups
Folders in parentheses organize without affecting URLs:
app/
├── (marketing)/ # Shared marketing layout
│ ├── layout.tsx
│ ├── about/page.tsx # /about
│ └── pricing/page.tsx # /pricing
├── (dashboard)/ # Shared dashboard layout
│ ├── layout.tsx
│ ├── settings/page.tsx # /settings
│ └── billing/page.tsx # /billingAPI Routes
route.ts files define API endpoints:
// app/api/bookings/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const bookings = await getBookings()
return NextResponse.json(bookings)
}
export async function POST(request: NextRequest) {
const data = await request.json()
const booking = await createBooking(data)
return NextResponse.json(booking, { status: 201 })
}Special Files
| File | Purpose |
|---|---|
layout.tsx | Shared UI wrapper for a route segment |
page.tsx | UI for a route |
loading.tsx | Loading UI (Suspense fallback) |
error.tsx | Error boundary |
not-found.tsx | 404 page |
route.ts | API endpoint |
Layouts
Layouts wrap their children and persist across navigation:
// app/dashboard/layout.tsx
import { Sidebar } from '@/components/layout/sidebar'
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
)
}Loading States
// app/accommodations/loading.tsx
export default function Loading() {
return (
<div className="grid grid-cols-4 gap-4">
{[...Array(8)].map((_, i) => (
<ListingCardSkeleton key={i} />
))}
</div>
)
}Error Handling
// app/accommodations/error.tsx
"use client"
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div className="text-center py-12">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}Navigation
Link Component
import Link from 'next/link'
<Link href="/accommodations">Browse Accommodations</Link>
// With dynamic segment
<Link href={`/accommodations/${listing.slug}`}>
{listing.title}
</Link>Programmatic Navigation
"use client"
import { useRouter } from 'next/navigation'
function BookingForm() {
const router = useRouter()
async function handleSubmit() {
const booking = await createBooking(data)
router.push(`/dashboard/bookings/${booking.reference}`)
}
}Redirects
In Server Components or Server Actions:
import { redirect } from 'next/navigation'
export default async function ProtectedPage() {
const user = await getCurrentUser()
if (!user) {
redirect('/login')
}
}Middleware
Global route handling in middleware.ts:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Auth check
const token = request.cookies.get('token')
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}Route Metadata
Static Metadata
// app/accommodations/page.tsx
export const metadata = {
title: 'Accommodations | Y3NKO',
description: 'Browse accommodations across Ghana'
}Dynamic Metadata
// app/accommodations/[slug]/page.tsx
export async function generateMetadata({ params }) {
const listing = await getListing(params.slug)
return {
title: `${listing.title} | Y3NKO`,
description: listing.description,
openGraph: {
images: [listing.images[0]?.url]
}
}
}Static Generation
Generate Static Params
Pre-render dynamic routes at build time:
// app/accommodations/[slug]/page.tsx
export async function generateStaticParams() {
const listings = await prisma.listing.findMany({
select: { slug: true },
where: { status: 'PUBLISHED' }
})
return listings.map((listing) => ({
slug: listing.slug
}))
}Routes with generateStaticParams are built at compile time, improving performance for high-traffic pages.