Next Starter Logo
Tutorials

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.

LocationURLLayoutAuth
app/(site)/publicHeader + Footernone
app/dashboard//dashboard/*sidebar + breadcrumbsigned-in required
app/dashboard/(admin)//dashboard/*same chromerole === "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).

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 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.

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-in if the user is signed out
  • it redirects to /onboarding if 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.

app/dashboard/reports/page.tsx
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 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).

components/dashboard/sidebar.tsx
{ name: "Reports", href: "/dashboard/reports", icon: BarChart, group: "main" },
GroupShown toWhere
maineveryonetop of sidebar
systemadmins only (role === "admin")under a "SYSTEM" label
secondaryeveryonebottom (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/:id

Read dynamic params by awaiting the params prop:

app/dashboard/reports/[id]/page.tsx
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.

OptionEffect
titlerenders as Title - App Name. Set titleFirst: true to flip it to App Name - Title
descriptionmeta + Open Graph description
pathnamebuilds the canonical URL
noIndexadds robots: noindex, nofollow. Use it on every dashboard and auth page

Two other files control crawling, separate from per-page noIndex:

  • app/robots.ts builds its disallow list from APP_CONFIG.noIndexRoutes in lib/config.ts. Add a path there to block crawlers from a whole route.
  • app/sitemap.ts lists pages from its own publicPages array. A new public page only appears in the sitemap after you add it to that array.

On this page