Use Cases (Aktualisiert: 2.6.2026)

Pagination mit Claude Code, React und Next.js implementieren

Praxisguide für Pagination mit Claude Code: URL-State, API-Metadaten, Accessibility und Grenzfälle.

Pagination mit Claude Code, React und Next.js implementieren

Pagination wirkt wie ein kleines UI-Muster: Zurück, Weiter und ein paar Seitenzahlen. In echten Produkten liegt das Risiko aber selten im Aussehen der Buttons. Entscheidend ist, dass die URL die Quelle der Wahrheit bleibt, Suchfilter erhalten werden, ungültige Seitenzahlen sicher korrigiert werden, die letzte Seite bei Datenänderungen nicht bricht und assistive Technologien die aktuelle Seite erkennen können.

Bei Tests in ClaudeCodeLab-Artikelarchiven und adminähnlichen Tabellen traten nach der ersten Generierung immer wieder dieselben Fehler auf: page=0 zeigte eine leere Liste, Filter gingen in den Links verloren, die letzte Seite war nach Löschungen falsch, oder die aktuelle Seite wurde nur durch Farbe markiert. Claude Code kann diese Details sauber umsetzen, wenn der Prompt die Regeln und Prüffälle von Anfang an nennt.

Dieser Guide zeigt, wie du Claude Code eine robuste Pagination mit React und Next.js App Router bauen lässt. Enthalten sind Prompt, URL-Design, serverseitiges Slicing, JSON-API, barrierearme Komponente, mehr als drei Use Cases, konkrete Fallstricke, offizielle Quellen, interne Links, CTA und ein Verifikationshinweis. Wenn eine Liste kontinuierlich laden soll, vergleiche mit Infinite Scroll. Für API-Verträge hilft REST API Design. Für Tastatur und Screenreader lies Accessibility mit Claude Code.

Modell wählen

Es gibt zwei Grundmodelle. Offset-Pagination fragt nach “Seite 3, 10 Elemente pro Seite”. Sie passt zu Artikelarchiven, Suchergebnissen, Produktlisten und Admin-Tabellen, weil jede Seite eine stabile URL haben kann. Cursor-Pagination fragt nach “10 Elemente nach dieser ID”. Sie ist besser für Benachrichtigungen, Audit-Logs, Chats und Timelines, in denen während des Lesens neue Datensätze entstehen.

Hier verwenden wir Offset-Pagination, weil sie für SEO, Browser-Historie und geteilte Links gut funktioniert. Eine URL wie /articles?page=3&q=react lässt sich direkt öffnen, weitergeben und nach einem Reload rekonstruieren. Bei Echtzeit-Feeds solltest du Claude Code ausdrücklich Cursor-Pagination vorgeben, sonst können neue Einträge Duplikate oder Lücken erzeugen.

ModellGeeignet fürHauptrisiko
OffsetArtikel, Suche, Produkte, Admin-TabellenDie letzte Seite verschiebt sich bei geänderter Gesamtzahl
CursorNotifications, Audit-Logs, Chat, TimelineDirekter Sprung zu beliebiger Seite ist schwierig
Infinite ScrollFeeds, Galerien, EmpfehlungenZurück-Navigation, Footer und SEO brauchen Zusatzarbeit

Die offizielle Claude Code Overview beschreibt Claude Code als agentisches Coding-Tool, das Codebasen liest, Dateien bearbeitet, Befehle ausführt und sich in Entwicklungstools integriert. Genau deshalb sollte der Auftrag nicht nur “UI bauen” lauten, sondern URL, API, Accessibility und Tests umfassen.

Prompt für Claude Code

Pagination berührt UI, Routing, Datenzugriff und Accessibility. Der erste Prompt sollte die Definition of Done festlegen.

Implementiere eine Artikel-Pagination mit React und Next.js App Router.
Anforderungen:
- URL-Parameter page und q sind die Quelle der Wahrheit
- Modernes Next.js mit searchParams als Promise in page.tsx unterstützen
- 10 Elemente pro Seite; page=0 und nicht numerische Werte werden zu 1
- Wenn die angefragte Seite größer als die letzte ist, zeige die letzte Seite
- Der Link der aktuellen Seite bekommt aria-current="page"
- Deaktivierte Zurück/Weiter-Controls als span rendern, nicht als klickbare Links
- Vorhandene frontmatter, heroImage, interne Links und Locale-Routen nicht brechen
- Nach der Implementierung Grenzfalltests auflisten

Im modernen Next.js App Router wird searchParams in page.tsx als Promise behandelt. Die offizielle page.js-Referenz zeigt den Zugriff mit await. In Client Components liest useSearchParams die Query, gibt aber eine schreibgeschützte URLSearchParams-Ansicht zurück.

URL-State

Das folgende Beispiel läuft als Server Component. Es liest q und page, normalisiert die Seite, erhält den Filter und übergibt sichere Werte an Pagination. Die Datenquelle ist ein Array, damit der Code direkt nachvollziehbar bleibt. In einem echten Projekt ersetzt du Filterung und Slice durch eine DB-Abfrage mit denselben Regeln.

import { Pagination } from "@/components/Pagination";

const PAGE_SIZE = 10;

const articles = Array.from({ length: 87 }, (_, index) => ({
  id: `article-${index + 1}`,
  title: `Claude Code pagination note ${index + 1}`,
  createdAt: new Date(Date.UTC(2026, 0, index + 1)).toISOString(),
}));

type SearchParams = Promise<{
  page?: string;
  q?: string;
}>;

function readPage(value: string | undefined) {
  const page = Number(value ?? "1");
  return Number.isInteger(page) && page > 0 ? page : 1;
}

export default async function ArticlesPage({
  searchParams,
}: {
  searchParams: SearchParams;
}) {
  const params = await searchParams;
  const query = params.q?.trim() ?? "";
  const requestedPage = readPage(params.page);

  const filtered = query
    ? articles.filter((article) =>
        article.title.toLowerCase().includes(query.toLowerCase()),
      )
    : articles;

  const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  const currentPage = Math.min(requestedPage, totalPages);
  const start = (currentPage - 1) * PAGE_SIZE;
  const visibleArticles = filtered.slice(start, start + PAGE_SIZE);

  return (
    <main className="mx-auto max-w-3xl px-4 py-10">
      <h1 className="text-3xl font-bold">Articles</h1>

      <form action="/articles" className="mt-6 flex gap-2">
        <input
          type="search"
          name="q"
          defaultValue={query}
          placeholder="Search articles"
          className="min-w-0 flex-1 rounded border px-3 py-2"
        />
        <button className="rounded bg-black px-4 py-2 text-white">Search</button>
      </form>

      <p className="mt-4 text-sm text-gray-600">
        {filtered.length} articles, page {currentPage} of {totalPages}
      </p>

      <ul className="mt-6 divide-y">
        {visibleArticles.map((article) => (
          <li key={article.id} className="py-4">
            <h2 className="font-semibold">{article.title}</h2>
            <time className="text-sm text-gray-500" dateTime={article.createdAt}>
              {new Intl.DateTimeFormat("en").format(new Date(article.createdAt))}
            </time>
          </li>
        ))}
      </ul>

      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        basePath="/articles"
        query={{ q: query || undefined }}
      />
    </main>
  );
}

Wichtig ist: Die Seite lebt nicht nur in React State. Wenn der Zustand nicht in der URL steckt, werden Reload, Teilen, Indexierung und Zurück-Navigation fragil. Für Query Strings ist URLSearchParams die Standard-API; Details stehen bei MDN URLSearchParams.

JSON-API

Wenn mobile Apps, Widgets oder Client-Tabellen dieselben Daten brauchen, stelle eine API mit klaren Metadaten bereit. Akzeptiere nie beliebige pageSize-Werte.

import type { NextRequest } from "next/server";

const MAX_PAGE_SIZE = 50;

const articles = Array.from({ length: 87 }, (_, index) => ({
  id: `article-${index + 1}`,
  title: `Claude Code pagination note ${index + 1}`,
  createdAt: new Date(Date.UTC(2026, 0, index + 1)).toISOString(),
}));

function readPositiveInt(value: string | null, fallback: number) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

export async function GET(request: NextRequest) {
  const page = readPositiveInt(request.nextUrl.searchParams.get("page"), 1);
  const requestedSize = readPositiveInt(
    request.nextUrl.searchParams.get("pageSize"),
    10,
  );
  const pageSize = Math.min(requestedSize, MAX_PAGE_SIZE);
  const totalItems = articles.length;
  const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
  const safePage = Math.min(page, totalPages);
  const start = (safePage - 1) * pageSize;

  return Response.json({
    items: articles.slice(start, start + pageSize),
    meta: {
      page: safePage,
      pageSize,
      totalItems,
      totalPages,
      hasPreviousPage: safePage > 1,
      hasNextPage: safePage < totalPages,
    },
  });
}

Die Datei passt nach app/api/articles/route.ts. Die offiziellen Next.js Route Handler Docs beschreiben route.ts im app-Verzeichnis. In Produktion liefert die Datenschicht sichtbare Zeilen plus eine exakte oder bewusst approximierte Gesamtzahl.

Accessible Component

Das Styling ist austauschbar, die Semantik nicht. Nutze ein gelabeltes nav, markiere genau eine aktuelle Seite mit aria-current="page" und rendere deaktivierte Controls nicht als aktive Links. Die MDN aria-current-Referenz nennt Pagination ausdrücklich als Beispiel.

import Link from "next/link";

type QueryValue = string | number | undefined;

type PaginationProps = {
  currentPage: number;
  totalPages: number;
  basePath: string;
  query?: Record<string, QueryValue>;
  previousLabel?: string;
  nextLabel?: string;
};

function normalizePage(page: number, totalPages: number) {
  return Math.min(Math.max(1, page), Math.max(1, totalPages));
}

function visiblePages(currentPage: number, totalPages: number) {
  const pages = new Set([1, totalPages, currentPage - 1, currentPage, currentPage + 1]);
  return [...pages]
    .filter((page) => page >= 1 && page <= totalPages)
    .sort((a, b) => a - b);
}

function hrefForPage(
  basePath: string,
  query: Record<string, QueryValue>,
  page: number,
) {
  const params = new URLSearchParams();

  for (const [key, value] of Object.entries(query)) {
    if (value !== undefined && value !== "") params.set(key, String(value));
  }

  if (page === 1) {
    params.delete("page");
  } else {
    params.set("page", String(page));
  }

  const queryString = params.toString();
  return queryString ? `${basePath}?${queryString}` : basePath;
}

export function Pagination({
  currentPage,
  totalPages,
  basePath,
  query = {},
  previousLabel = "Previous",
  nextLabel = "Next",
}: PaginationProps) {
  if (totalPages <= 1) return null;

  const safePage = normalizePage(currentPage, totalPages);
  const pages = visiblePages(safePage, totalPages);

  return (
    <nav className="mt-8" aria-label="Pagination">
      <ol className="flex flex-wrap items-center gap-2">
        <li>
          {safePage === 1 ? (
            <span aria-disabled="true" className="rounded border px-3 py-2 opacity-50">
              {previousLabel}
            </span>
          ) : (
            <Link
              className="rounded border px-3 py-2 hover:bg-gray-50"
              href={hrefForPage(basePath, query, safePage - 1)}
            >
              {previousLabel}
            </Link>
          )}
        </li>

        {pages.map((page, index) => {
          const previous = pages[index - 1];
          const needsGap = previous !== undefined && page - previous > 1;

          return (
            <li key={page} className="flex items-center gap-2">
              {needsGap ? <span aria-hidden="true">...</span> : null}
              <Link
                aria-current={page === safePage ? "page" : undefined}
                className={
                  page === safePage
                    ? "rounded border bg-black px-3 py-2 text-white"
                    : "rounded border px-3 py-2 hover:bg-gray-50"
                }
                href={hrefForPage(basePath, query, page)}
              >
                {page}
              </Link>
            </li>
          );
        })}

        <li>
          {safePage === totalPages ? (
            <span aria-disabled="true" className="rounded border px-3 py-2 opacity-50">
              {nextLabel}
            </span>
          ) : (
            <Link
              className="rounded border px-3 py-2 hover:bg-gray-50"
              href={hrefForPage(basePath, query, safePage + 1)}
            >
              {nextLabel}
            </Link>
          )}
        </li>
      </ol>
    </nav>
  );
}

Diagramm und Review

Bitte Claude Code nach der Implementierung um ein kleines Diagramm. Es zeigt, ob URL, Server Page, Liste und Komponente sauber getrennt sind.

flowchart LR
  A["URL: /articles?page=3&q=react"] --> B["Page searchParams"]
  B --> C["readPage and filter"]
  C --> D["slice visible items"]
  D --> E["Article list"]
  C --> F["Pagination component"]
  F --> A
  C --> G["Optional JSON API meta"]

Die Review-Frage lautet: Kann der komplette Zustand aus der URL rekonstruiert werden? Wenn ja, sind Reload, Zurück, Teilen und SEO deutlich robuster.

Use Cases

Erstens: Blog- oder Dokumentationsarchive. Wenn die Zahl der Claude-Code-Artikel wächst, bleibt die erste Seite leicht und ältere Inhalte bleiben direkt erreichbar.

Zweitens: Suche in Ecommerce oder SaaS. Suchtext, Kategorie, Preis, Sortierung und Seite gehören in die URL. Sage Claude Code ausdrücklich, dass Filteränderungen page auf 1 zurücksetzen sollen.

Drittens: Admin-Tabellen für Rechnungen, Nutzer, Formularanfragen oder Audit-Daten. Hier zählen Page-Size-Limits, Berechtigungsfilter und Konsistenz mit CSV-Exporten.

Viertens: Lern-Dashboards. Leser kommen oft Tage später zurück. Eine stabile Pagination hält die Position und erlaubt Rückkehr von CTAs wie kostenloser Cheat Sheet oder Claude-Code-Beratung.

Häufige Fallen

Vertraue nie der eingehenden Seitenzahl. page=-1, page=abc und page=9999 müssen serverseitig abgefangen werden. Verliere keine Filter beim Linkbau; ersetze nur page. Markiere die aktuelle Seite nicht nur farblich, sondern mit genau einem aria-current="page". Unterschätze außerdem nicht die Kosten von Counts auf großen Tabellen; ein exaktes COUNT(*) braucht oft Indexe, Cache oder bewusste Approximation.

Lege auch das History-Verhalten fest. Die Low-Level-API pushState() fügt einen Eintrag zum Browser-Session-History-Stack hinzu, wie MDN History pushState beschreibt. In Next.js verwendest du meist Link oder router.push, solltest aber bewusst zwischen Push und Replace wählen.

Verifiziertes Ergebnis

Ich habe das Beispiel mit folgenden Fällen geprüft: kein page, page=1, page=0, page=abc, page=9999, Suche mit Treffern, Suche ohne Treffer, letzte Seite und Ergebnis mit nur einer Seite. Am nützlichsten waren das Entfernen von page=1 aus der URL und das Begrenzen zu hoher Seiten nach dem Filtern. Geteilte Links werden sauberer und leere Seiten nach Datenänderungen seltener.

Vor der Veröffentlichung sollte Claude Code noch prüfen: genau ein aria-current, deaktivierte Zurück/Weiter-Controls an Grenzen, gedeckeltes pageSize, erhaltene Filter, await searchParams und gültige TypeScript-Beispiele. Pagination ist klein, beeinflusst aber Archive, Suche, Admin-Tools und Conversion-Pfade.

#Claude Code #Pagination #React #Next.js #UX
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.