Adding a New Page
How to add a new page in Next.js with the App Router. Where the file goes, how layouts and auth apply, and how to set page metadata and navigation links.
The App Router splits the app into two layout trees. Pick the right one and the rest (auth, chrome, redirects) is handled for you.
| Location | URL | Layout | Auth |
|---|---|---|---|
app/(site)/ | public | Header + Footer | none |
app/dashboard/ | /dashboard/* | sidebar + breadcrumb | signed-in required |
app/dashboard/(admin)/ | /dashboard/* | same chrome | role === "admin" required |
(site) and (admin) are route groups. The parentheses keep them out of the URL.
Marketing page
A marketing page is any public page under app/(site)/. The layout app/(site)/layout.tsx wraps it with the public Header and Footer. That layout reads the session only to fill the nav. It never redirects, so the page stays public.
Create the page file
Add app/(site)/case-studies/page.tsx. The folder name becomes the URL (/case-studies).
import type { Metadata } from "next";
import { generateMeta } from "@/lib/config";
export const metadata: Metadata = generateMeta({
title: "Case Studies",
description: "How teams ship faster with Next Starter.",
pathname: "/case-studies",
});
export default function CaseStudiesPage() {
return <div className="bg-background">{/* sections */}</div>;
}Keep it a server component
Leave the page as a server component so search engines can read it. Add "use client" only to the interactive piece (a form, a tab bar), not the whole page.
For a long page, build each visual section as its own file and compose them in page.tsx. The homepage (app/(site)/page.tsx) and app/(site)/about/page.tsx show the pattern.
Link to it
The header and footer read their links from arrays. Add an entry to NAV_LINKS in components/header.tsx. If the page also belongs in the footer, add it to the matching section of the navigation array in components/footer.tsx.
Dashboard page
A dashboard page is any page under app/dashboard/. The layout app/dashboard/layout.tsx does the guarding for every page below it:
- it calls
getSession()and redirects to/auth/sign-inif the user is signed out - it redirects to
/onboardingif the user has not finished onboarding - it renders the sidebar, the mobile header, and the breadcrumb
So auth is already enforced for you. You only call getSession() yourself when you need the user's id for a query.
Create the page file
Add app/dashboard/reports/page.tsx. Pass noIndex: true so search engines skip it.
import type { Metadata } from "next";
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(); // only if you need session.user.id
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold">Reports</h1>
{/* content */}
</div>
);
}Add the sidebar link
Add an entry to the navigationItems array in components/dashboard/sidebar.tsx. Pick an icon from lucide-react and set the group (the table below explains each one).
{ name: "Reports", href: "/dashboard/reports", icon: BarChart, group: "main" },| Group | Shown to | Where |
|---|---|---|
main | everyone | top of sidebar |
system | admins only (role === "admin") | under a "SYSTEM" label |
secondary | everyone | bottom (Billing, Settings) |
The active link is matched by path. /dashboard matches only when the path is exactly /dashboard. Every other link matches when the path equals its href or starts with href + "/", so a child route (like /dashboard/reports/123) keeps the parent link highlighted.
Admin-only page
To restrict a page to admins, put it under app/dashboard/(admin)/. That group's layout (app/dashboard/(admin)/layout.tsx) redirects any non-admin to /dashboard.
(admin) is a route group, so it never shows in the URL: a file at app/dashboard/(admin)/reports/page.tsx still serves /dashboard/reports. Give its sidebar link group: "system" so only admins see it.
Nested and dynamic routes
app/dashboard/reports/
├── page.tsx # /dashboard/reports
└── [id]/page.tsx # /dashboard/reports/:idRead dynamic params by awaiting the params prop:
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
}The breadcrumb (components/dashboard/breadcrumb.tsx) is automatic: it splits the path, drops dashboard and UUID segments, and title-cases the rest. To rename a segment's label, adjust the formatting logic there.
Metadata
Both page types use generateMeta from lib/config.ts, which merges your values with the site defaults in APP_CONFIG.
| Option | Effect |
|---|---|
title | renders as Title - App Name. Set titleFirst: true to flip it to App Name - Title |
description | meta + Open Graph description |
pathname | builds the canonical URL |
noIndex | adds robots: noindex, nofollow. Use it on every dashboard and auth page |
Two other files control crawling, separate from per-page noIndex:
app/robots.tsbuilds itsdisallowlist fromAPP_CONFIG.noIndexRoutesinlib/config.ts. Add a path there to block crawlers from a whole route.app/sitemap.tslists pages from its ownpublicPagesarray. A new public page only appears in the sitemap after you add it to that array.
Build Your First SaaS Feature
Build your first SaaS feature in Next.js: a Prisma model, a migration, a validated Server Action, and an auth-protected dashboard page for a notes feature.
Customizing the Theme
How to customize the theme in a Next.js app: change brand colors, corner radius, fonts, and dark mode through the Tailwind CSS v4 variables in globals.css.