Next Starter Logo
Tutorials

Adding a New Page

Step-by-step guide to adding authenticated dashboard pages and public marketing pages in Next Starter, including metadata, sidebar navigation, and breadcrumb support.

The project uses Next.js App Router with two distinct layout trees: app/(site)/ for marketing pages and app/dashboard/ for the authenticated user area. Each layout handles its own navigation, authentication, and styling.


Dashboard Pages

How the Layout Works

Every file inside app/dashboard/ is automatically wrapped by app/dashboard/layout.tsx. That layout:

  • Fetches the session with getSession() and redirects unauthenticated users to /auth/sign-in
  • Renders the sidebar, mobile header, and breadcrumb navigation
  • Provides the SubscriptionProvider and SidebarProvider context

You do not need to add authentication logic to individual dashboard pages unless you need the user's ID for database queries. In that case, call getSession() and handle the redirect defensively.

Create the Page File

Create a new file at the appropriate path inside app/dashboard/. For a page at /dashboard/reports:

app/
└── dashboard/
    └── reports/
        └── page.tsx
app/dashboard/reports/page.tsx
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { generateMeta } from "@/lib/config";
import { getSession } from "@/lib/server/auth-helpers";

export const metadata: Metadata = generateMeta({
  title: "Reports",
  description: "View your usage reports.",
  pathname: "/dashboard/reports",
  noIndex: true,
});

export default async function ReportsPage() {
  const session = await getSession();
  if (!session) redirect("/auth/sign-in");

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-semibold">Reports</h1>
        <p className="text-muted-foreground mt-1">Your usage over time.</p>
      </div>
      {/* page content */}
    </div>
  );
}

Use generateMeta from lib/config for consistent metadata. Pass noIndex: true for all dashboard pages to exclude them from search engine indexing.

Open components/dashboard/sidebar.tsx and add an entry to navigationItems. Choose an icon from lucide-react and assign it to one of three groups:

GroupPurpose
"main"Primary navigation visible to all users
"admin"Only shown to users with role === "admin"
"secondary"Bottom of the sidebar (Billing, Settings)
components/dashboard/sidebar.tsx
import { BarChart } from "lucide-react";

const navigationItems: NavigationItem[] = [
  { name: "Dashboard", href: "/dashboard", icon: Home, group: "main" },
  { name: "Reports", href: "/dashboard/reports", icon: BarChart, group: "main" }, // add this
  // ... rest of items
];

The sidebar uses pathname.startsWith(item.href + "/") for active state detection, so nested routes like /dashboard/reports/detail will also highlight the Reports link.

Nested Routes and Layouts

If your feature needs multiple sub-pages, add a nested layout:

app/
└── dashboard/
    └── reports/
        ├── layout.tsx     # optional nested layout
        ├── page.tsx       # /dashboard/reports
        └── [id]/
            └── page.tsx   # /dashboard/reports/[id]

Access dynamic segment parameters by awaiting the params prop:

app/dashboard/reports/[id]/page.tsx
interface Props {
  params: Promise<{ id: string }>;
}

export default async function ReportDetailPage({ params }: Props) {
  const { id } = await params;
  // fetch report by id
}

Marketing Pages

How the Layout Works

Files inside app/(site)/ share the layout at app/(site)/layout.tsx. This layout renders the public navigation header and footer. The (site) folder name is a route group: it does not appear in the URL.

Create the Page File

For a new page at /case-studies:

app/
└── (site)/
    └── case-studies/
        └── page.tsx
app/(site)/case-studies/page.tsx
import type { Metadata } from "next";
import { generateMeta } from "@/lib/config";

export const metadata: Metadata = generateMeta({
  title: "Case Studies",
  description: "How teams use Next Starter to ship faster.",
  pathname: "/case-studies",
});

export default function CaseStudiesPage() {
  return (
    <div className="bg-background">
      <section className="py-16 sm:py-24">
        <div className="mx-auto max-w-4xl px-6">
          <h1 className="text-4xl font-semibold tracking-tight text-foreground">
            Case Studies
          </h1>
          <p className="mt-4 text-lg text-muted-foreground">
            Real-world examples of what you can build.
          </p>
        </div>
      </section>
    </div>
  );
}

Keep marketing pages as server components by default for better performance and SEO. Convert to client components only when you need interactivity.

Sections vs. Pages

The homepage at app/(site)/page.tsx composes several section components (Hero, LogoCloud, FeatureLeft, FeatureRight, Bento, FAQ, CallToAction) imported from the same directory. For complex marketing pages, follow the same pattern: build each visual section as its own component file and compose them in page.tsx.

app/(site)/page.tsx
import Hero from "./hero";
import LogoCloud from "./logo-cloud";
import FeatureLeft from "./feature-left";
import FeatureRight from "./feature-right";
import Bento from "./bento";
import FAQ from "./faq";
import CallToAction from "./call-to-action";

export default function Home() {
  return (
    <div className="bg-background">
      <Hero />
      <LogoCloud />
      <FeatureLeft />
      <FeatureRight />
      <Bento />
      <FAQ />
      <CallToAction />
    </div>
  );
}

How the Breadcrumb Works

The dashboard breadcrumb at components/dashboard/breadcrumb.tsx reads the current URL path and generates labels automatically. It splits the pathname on /, filters out dashboard, replaces dashes with spaces, and capitalizes each word. UUID-format segments are filtered out automatically. To use a custom label for a segment, update the displayName formatting logic inside the breadcrumb component.


Metadata

Both page types use generateMeta from lib/config.ts to produce consistent <title> and <meta> tags. The function merges your values with the site-wide defaults defined in APP_CONFIG.

export const metadata: Metadata = generateMeta({
  title: "Page Title",           // becomes "<title>Page Title - App Name</title>"
  description: "...",
  pathname: "/your-path",        // used to build the canonical URL
  noIndex: true,                 // adds <meta name="robots" content="noindex, nofollow">
});

Pass noIndex: true for any page that should not appear in search results: all dashboard pages, auth pages, and any preview or staging content.

On this page