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 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.
| Modell | Geeignet für | Hauptrisiko |
|---|---|---|
| Offset | Artikel, Suche, Produkte, Admin-Tabellen | Die letzte Seite verschiebt sich bei geänderter Gesamtzahl |
| Cursor | Notifications, Audit-Logs, Chat, Timeline | Direkter Sprung zu beliebiger Seite ist schwierig |
| Infinite Scroll | Feeds, Galerien, Empfehlungen | Zurü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.
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.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.