Zum Inhalt springen
Next.js Anfรคnger 25 min

Pages & App Router

File-based Routing in Next.js: Pages, Layouts, Dynamic Routes, Route Groups, Loading und Error UI. Die Basis des App Routers.

Aktualisiert:
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.tsx in einem Ordner = eine Route
  • layout.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.

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-all
  • loading.tsx, error.tsx, not-found.tsx als spezielle Dateien
  • (group) = Route Groups ohne URL-Effekt
  • @name = Parallel Routes
  • route.ts = API-Route
  • metadata / generateMetadata fuer SEO
  • generateStaticParams fuer Static Generation

Im naechsten Kapitel: Der wichtigste Shift - Server Components vs. Client Components.

Zurรผck zum Next.js Kurs