Pages & App Router
File-based Routing in Next.js: Pages, Layouts, Dynamic Routes, Route Groups, Loading und Error UI. Die Basis des App Routers.
Inhaltsverzeichnis
Pages & App Router
Im App Router ist deine Ordnerstruktur gleichzeitig dein Routing. Kein React-Router, keine Config - das Dateisystem ist die Wahrheit.
Die Grundregel
page.tsxin einem Ordner = eine Routelayout.tsx= umschliesst die Routen darunter- Ordner = Teil der URL
Einfache Routen
app/
โโโ page.tsx โ /
โโโ about/
โ โโโ page.tsx โ /about
โโโ blog/
โ โโโ page.tsx โ /blog
โโโ kontakt/
โโโ page.tsx โ /kontakt
Jede page.tsx ist eine React-Komponente (Default-Export).
// app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>Ueber uns</h1>
</main>
);
}
Aufruf http://localhost:3000/about.
Navigation mit <Link>
import Link from "next/link";
export default function Home() {
return (
<nav>
<Link href="/about">Ueber uns</Link>
<Link href="/blog">Blog</Link>
<Link href="/kontakt">Kontakt</Link>
</nav>
);
}
- Kein Page-Reload - Client-Side-Navigation
- Automatisches Prefetching der Zielseite im Hintergrund
- Gefuellte URL-Bar
Layouts
Layouts umschliessen Routen - perfekt fuer Navigation, Footer, globale Konfiguration.
app/
โโโ layout.tsx โ Root-Layout (alle Routen)
โโโ page.tsx
โโโ dashboard/
โ โโโ layout.tsx โ nur fuer /dashboard/*
โ โโโ page.tsx โ /dashboard
โ โโโ settings/
โ โโโ page.tsx โ /dashboard/settings
app/dashboard/layout.tsx:
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="w-64 bg-gray-100 p-4">
<nav>
<Link href="/dashboard">Ueberblick</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
</aside>
<main className="flex-1 p-6">{children}</main>
</div>
);
}
Das Sidebar ist bei /dashboard und /dashboard/settings sichtbar.
Wichtiger Vorteil: Layouts bleiben beim Navigieren erhalten - Sidebar-State, Scroll-Position etc. Werden nicht neu gerendert.
Dynamic Routes
Platzhalter im Ordnernamen mit [...]:
app/
โโโ blog/
โโโ [slug]/
โโโ page.tsx โ /blog/:slug
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<article>
<h1>Post: {slug}</h1>
</article>
);
}
In Next.js 15+ sind params async - beachte das await.
Catch-all
app/
โโโ docs/
โโโ [...path]/
โโโ page.tsx
Matcht /docs/a, /docs/a/b, /docs/a/b/c/d.
export default async function DocsPage({
params,
}: {
params: Promise<{ path: string[] }>;
}) {
const { path } = await params;
return <div>Pfad: {path.join("/")}</div>;
}
Optional Catch-all
Mit [[...path]] - matcht auch den Root.
Loading UI
Eine loading.tsx zeigt einen Loading-State waehrend der Server rendert:
app/
โโโ dashboard/
โ โโโ layout.tsx
โ โโโ loading.tsx โ zeigt waehrend Page laedt
โ โโโ page.tsx
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center h-64">
<span className="animate-spin">Laedt...</span>
</div>
);
}
Waehrend die (async) page.tsx ihre Daten holt, sieht der User diesen Spinner.
Error UI
Analog mit error.tsx:
// app/dashboard/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Etwas ist schiefgelaufen!</h2>
<button onClick={reset}>Nochmal versuchen</button>
</div>
);
}
"use client" ist Pflicht, weil onClick ein Event-Handler ist.
Not Found
// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <h1>Dieser Post existiert nicht.</h1>;
}
Wird angezeigt, wenn die Page notFound() aufruft:
import { notFound } from "next/navigation";
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await findPost(slug);
if (!post) notFound();
return <Article post={post} />;
}
Route Groups
Mit Klammern (...) gruppierst du Ordner ohne Einfluss auf die URL:
app/
โโโ (marketing)/
โ โโโ layout.tsx โ nur fuer marketing-Seiten
โ โโโ page.tsx โ /
โ โโโ pricing/
โ โโโ page.tsx โ /pricing
โโโ (app)/
โ โโโ layout.tsx โ nur fuer App-Seiten
โ โโโ dashboard/
โ โ โโโ page.tsx โ /dashboard
โ โโโ settings/
โ โโโ page.tsx โ /settings
Zwei unterschiedliche Layouts, gleiche URL-Struktur.
Parallel Routes
Mit @name-Ordnern kannst du mehrere Slots parallel rendern:
app/
โโโ layout.tsx
โโโ @modal/
โ โโโ default.tsx
โโโ @sidebar/
โโโ default.tsx
Das Layout bekommt dann mehrere children-Props:
export default function Layout({
children,
modal,
sidebar,
}: {
children: React.ReactNode;
modal: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<>
<aside>{sidebar}</aside>
<main>{children}</main>
{modal}
</>
);
}
Metadata
Meta-Tags direkt in der Page oder Layout:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Blog",
description: "Unser Blog",
openGraph: {
images: ["/og-image.jpg"],
},
};
export default function BlogPage() {
return <main>...</main>;
}
Fuer dynamische Metadaten:
export async function generateMetadata({
params,
}: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.titel,
description: post.beschreibung,
};
}
generateStaticParams
Damit werden dynamische Seiten zur Build-Zeit statisch gebaut:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
// ...
}
Bei npm run build werden alle slugs pre-generated - die Seiten werden als statisches HTML ausgeliefert.
API Routes
Dateien heissen route.ts statt page.tsx:
app/
โโโ api/
โโโ users/
โโโ route.ts โ GET/POST auf /api/users
export async function GET() {
const users = await db.users.findMany();
return Response.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.users.create({ data: body });
return Response.json(user, { status: 201 });
}
Dynamic API Routes wie Pages:
app/api/users/[id]/route.ts
Programmatic Navigation
Mit dem Hook useRouter (Client Components):
"use client";
import { useRouter } from "next/navigation";
export default function LoginButton() {
const router = useRouter();
function handleLogin() {
// ... Login-Logik
router.push("/dashboard");
}
return <button onClick={handleLogin}>Login</button>;
}
URL-Params lesen (Client)
"use client";
import { useSearchParams } from "next/navigation";
export default function SearchPage() {
const params = useSearchParams();
const q = params.get("q");
return <div>Suche: {q}</div>;
}
Fuer Server: searchParams als Prop:
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <div>Suche: {q}</div>;
}
Zusammenfassung
page.tsx= Route,layout.tsx= Wrapper fuer alles darunter[slug]= dynamic,[...path]= catch-allloading.tsx,error.tsx,not-found.tsxals spezielle Dateien(group)= Route Groups ohne URL-Effekt@name= Parallel Routesroute.ts= API-Routemetadata/generateMetadatafuer SEOgenerateStaticParamsfuer Static Generation
Im naechsten Kapitel: Der wichtigste Shift - Server Components vs. Client Components.