Documentation is currently in beta. Report issues →

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 page

Route 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  # /billing

API 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

FilePurpose
layout.tsxShared UI wrapper for a route segment
page.tsxUI for a route
loading.tsxLoading UI (Suspense fallback)
error.tsxError boundary
not-found.tsx404 page
route.tsAPI 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>
  )
}
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.