Routing Guide

Routing Guide

Master Next.js 16 App Router patterns used throughout SentinelGrid.

Routing Guide

SentinelGrid uses the Next.js 16 App Router. Every route is a folder with a page.tsx, layouts are shared via route groups, and dynamic segments use the [id] syntax.

App Router Fundamentals

The App Router is file-system based. Each folder under app/ maps to a URL segment. A folder becomes a route when it contains a page.tsx file.

App Router mapping
app/
├── page.tsx                  →  /
├── dashboard/
│   ├── page.tsx              →  /dashboard
│   └── incident-command/
│       └── page.tsx          →  /dashboard/incident-command
└── incidents/
    ├── page.tsx              →  /incidents
    └── [id]/
        └── page.tsx          →  /incidents/INC-123

Route Groups

Route groups use parentheses ((name)) to organize routes without affecting the URL. SentinelGrid uses three:

  • (dashboard) — every authenticated app page shares the sidebar + header shell
  • (auth) — sign in, sign up, onboarding, password reset
  • (marketing) — landing, features, pricing (not used in this template; marketing pages live at top-level routes instead)
src/app/(dashboard)/layout.tsx
"use client";

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen overflow-hidden bg-background">
      <Sidebar />
      <div className="flex-1 flex flex-col min-w-0">
        <Header />
        <main className="flex-1 overflow-y-auto">
          <div className="animate-fade-up">{children}</div>
        </main>
      </div>
      <RightPanel />
    </div>
  );
}

Why route groups?

Because (dashboard) does not appear in the URL, the route /(dashboard)/incidents/page.tsx resolves to /incidents, not /dashboard/incidents. This keeps URLs clean while still sharing a layout.

Dynamic Routes

Dynamic segments are created by wrapping a folder name in square brackets: [id]. Access the param with the useParams() hook on the client.

src/app/(dashboard)/incidents/[id]/page.tsx
"use client";
import { useParams } from "next/navigation";
import { incidents } from "@/lib/data";

export default function IncidentDetailPage() {
  const params = useParams<{ id: string }>();
  const incident = incidents.find((i) => i.id === params.id) ?? incidents[0];
  return (
    <PageContainer>
      <PageHeader title={incident.title} />
      {/* ... */}
    </PageContainer>
  );
}

For detail pages, always provide a graceful fallback when the ID is missing or invalid. SentinelGrid detail pages fall back to the first entity so the preview never breaks.

Layouts

Layouts wrap child pages and preserve state across navigations. The root layout in app/layout.tsx loads fonts, mounts ThemeProvider, and registers global toasters.

src/app/layout.tsx (excerpt)
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <ThemeProvider>
          <UIStoreHydration />
          {children}
          <CommandPalette />
          <Toaster />
          <SonnerToaster position="bottom-right" />
        </ThemeProvider>
      </body>
    </html>
  );
}

Nested layouts compose automatically. A page at /(dashboard)/admin/team/[id]/page.tsx renders inside three layouts: root, dashboard, and any layout in the admin folder.

Loading States

Add a loading.tsx file alongside any page.tsx to show a fallback during route transitions or async server work. SentinelGrid provides a reusable PageSkeleton component.

src/app/(dashboard)/incidents/loading.tsx
import { PageContainer, PageSkeleton } from "@/components/common/page-header";

export default function Loading() {
  return (
    <PageContainer>
      <PageSkeleton cards={4} />
    </PageContainer>
  );
}

Error Boundaries

An error.tsx file catches unhandled errors in any route segment. It must be a client component and accept error and reset props.

src/app/(dashboard)/error.tsx
"use client";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div className="flex flex-col items-center justify-center py-20 px-6 text-center">
      <AlertTriangle className="w-10 h-10 text-destructive mb-4" />
      <h2 className="text-lg font-semibold">Something went wrong</h2>
      <p className="text-sm text-muted-foreground mt-1 max-w-md">{error.message}</p>
      <Button onClick={reset} className="mt-4">Try again</Button>
    </div>
  );
}

Global 404

SentinelGrid ships a global not-found.tsx at the root that renders a branded 404 for unmatched routes. You can also add route-specific not-found.tsx files for finer control.

Command Palette

Search for a command to run...