So haben wir eine Verzeichnisplattform mit 137K Listings mit Next.js und Vercel ISR gebaut
Wie wir eine Verzeichnisplattform mit 137.000 Einträgen mit Next.js und Vercel ISR entwickelt haben
Im letzten Jahr haben wir eine Verzeichnisplattform gestartet. 137.000 Einträge. Das war kein Nebenprojekt. Es war eine vollständig realisierte Plattform, bei der jeder Eintrag eine eigene SEO-optimierte Seite hatte. Suchanfragen mussten blitzschnell sein, und ja, das Hosting musste erschwinglich bleiben. Wie haben wir das mit Next.js, Vercel und Incremental Static Regeneration (ISR) geschafft? Schnall dich an; hier ist die Geschichte, einschließlich der kniffligen Stellen.

Inhaltsverzeichnis
- Warum eine Verzeichnisplattform schwieriger ist, als es aussieht
- Architektur-Übersicht
- Die ISR-Strategie, die im großen Maßstab funktioniert
- 137.000 Seiten ohne explodierenden Build-Zeiten handhaben
- Datenbank und Suchschicht
- SEO im großen Maßstab: Sitemaps, strukturierte Daten und Crawl-Budget
- Leistungs-Benchmarks
- Kostenaufschlüsselung auf Vercel
- Fehler, die wir gemacht haben, und was wir ändern würden
- Häufig gestellte Fragen
Warum eine Verzeichnisplattform schwieriger ist, als es aussieht
Verzeichnisseiten mögen unkompliziert wirken. Man könnte denken, eine Listenseite, eine Detailseite, ein paar Filter hinzufügen, und voilà! Fertig. Aber sobald du über einige tausend Einträge hinausgegangen bist, wird alles unglaublich komplex.
Hier ist, was wirklich vor sich geht:
- 137.000+ eindeutige Seiten, die jeweils durchsuchbar und indexierbar sein müssen
- Facettierte Suche über Standort, Kategorie und mehr
- Verwaltung veralteter Daten -- Einträge sind in ständiger Bewegung mit Aktualisierungen und Löschungen
- SEO-Anforderungen bedeuten, dass du dich nicht einfach auf Client-seitiges Rendering verlassen kannst
- Hosting mit kleinerem Budget schließt das Generieren aller Seiten zum Build-Zeitpunkt aus
Nach der Prüfung verschiedener Methoden haben wir Next.js mit ISR als unsere erste Wahl bestätigt. Wir haben auch Astro in Betracht gezogen (das wir in einigen unserer anderen Projekte verwenden – siehe unsere Astro-Entwicklungsarbeiten). Letztendlich war die dynamische Kapazität von Next.js mit ISR die naheliegende Wahl.
Architektur-Übersicht
Hier sieht unsere Architektur aus:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Vercel │────▶│ Next.js App │────▶│ PostgreSQL │
│ Edge CDN │ │ (ISR) │ │ (Neon) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Redis │ │ Meilisearch │
│ (Upstash) │ │ (Cloud) │
└──────────────┘ └──────────────┘
Der Stack
| Komponente | Technologie | Begründung |
|---|---|---|
| Framework | Next.js 14 (App Router) | ISR-Unterstützung, React Server Components, Route Handler |
| Hosting | Vercel Pro | Edge CDN, ISR-Infrastruktur, Analytik |
| Datenbank | Neon PostgreSQL | Serverless Postgres, Branching für Vorschauen |
| Suche | Meilisearch Cloud | Tippfehlertoleranz, facettierte Suche, schnelle Indizierung |
| Cache | Upstash Redis | Rate Limiting, Session-Cache, ISR-Koordination |
| CMS (Admin) | Custom Admin + Payload CMS | Eintrags-Verwaltung, Massenoperationen |
| CDN/Bilder | Vercel Image Optimization + Cloudinary | Eintragsfotos mit mehreren Auflösungen |
Dies ist ein Next.js-Entwicklungs-Projekt im Kern, und ISR war der große Verkaufsargument für uns.

Die ISR-Strategie, die im großen Maßstab funktioniert
Kommen wir direkt zur Sache: Wenn du versuchen möchtest, 137.000 Seiten zum Build-Zeitpunkt statisch zu generieren, stellst du dich selbst vor Probleme. Ernsthaft, lade diese Kopfschmerzen nicht ein. Auch mit der parallelen Generierung von Next.js könnten Builds über 45 Minuten andauern und jeden Deployment zu einem Albtraum machen.
ISR ermöglicht es dir, Seiten nach Bedarf zu generieren und sie am Edge zu cachen. Standard-ISR ist großartig, aber für uns waren Anpassungen notwendig.
Die dreistufige Seitenstrategie
Wir unterteilten unsere Einträge in drei Stufen:
// app/listing/[slug]/page.tsx
export async function generateStaticParams() {
// Stufe 1: Die Top 2.000 Einträge mit dem höchsten Traffic vorgenerieren
const topListings = await db.listing.findMany({
where: { tier: 'premium' },
orderBy: { monthlyViews: 'desc' },
take: 2000,
select: { slug: true },
});
return topListings.map((listing) => ({
slug: listing.slug,
}));
}
// Stufe 2 & 3: On-Demand über ISR generiert
export const revalidate = 3600; // 1 Stunde für die meisten Einträge
export default async function ListingPage({ params }: { params: { slug: string } }) {
const listing = await getListingBySlug(params.slug);
if (!listing) {
notFound();
}
// Dynamische Revalidierung basierend auf Eintrag-Tier
// Premium-Einträge revalidieren alle 10 Minuten
// Standard-Einträge jede Stunde
// Archivierte Einträge alle 24 Stunden
return <ListingDetail listing={listing} />;
}
Stufe 1 (2.000 Seiten): Diese hochfrequenten Einträge werden zum Build-Zeitpunkt vorgeneriert. Sie sind verantwortlich für den meisten organischen Suchtraffic. Sie sind immer bereit.
Stufe 2 (35.000 Seiten): Wird beim ersten Zugriff generiert, für eine Stunde gecacht. Diese Einträge haben gleichmäßigen Traffic, also bekommt der erste Besucher nach Cache-Ablauf eine Server-gerenderte aber schnelle Seite. Alle anderen bekommen die gecachte Version.
Stufe 3 (100.000 Seiten): Wird beim ersten Zugriff generiert, für 24 Stunden gecacht. Diese Einträge sehen kaum Aktivität, also gibt es keinen Grund, Ressourcen zu verschwenden.
On-Demand Revalidation für Echtzeit-Updates
Die meisten Fälle werden durch zeitgesteuerte Revalidierungen abgedeckt, aber was ist mit der Restaurantinhaberin, die gerade ihre Öffnungszeiten aktualisiert hat? Nun, wir haben Next.js's On-Demand Revalidation mithilfe von Route Handlern eingeführt:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidation-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
const { slug, type } = await request.json();
if (type === 'listing') {
revalidatePath(`/listing/${slug}`);
revalidateTag(`listing-${slug}`);
} else if (type === 'category') {
revalidateTag(`category-${slug}`);
}
return NextResponse.json({ revalidated: true, now: Date.now() });
}
Mit unserem Admin-Panel und Webhooks, die mit diesem Endpoint kommunizieren, bekommt jeder Eintrag-Änderung eine frische Seite beim nächsten Zugriff. Schnell, nicht wahr?
137.000 Seiten ohne explodierenden Build-Zeiten handhaben
Build-Zeiten haben uns ehrlich gesagt verängstigt! Hier ist, was wir gefunden haben:
| Strategie | Build-Zeit | Erste Anfrage Latenz | Cache Hit Latenz |
|---|---|---|---|
| Vollständiger SSG (alle 137K Seiten) | ~52 Minuten | ~40ms | ~40ms |
| ISR (2K vorgefertigt) | ~3,5 Minuten | ~180ms (kalt) | ~40ms |
| Vollständiger SSR (kein Caching) | ~45 Sekunden | ~250ms | N/A |
| Unser Hybrid-Ansatz | ~3,5 Minuten | ~150ms (kalt) | ~35ms |
Unser ISR-Ansatz reduzierte Build-Zeiten von quälenden 60 Minuten auf knapp unter 4 Minuten. Das ist der Unterschied zwischen dem Fürchten von Deployments und, naja, Kaffee trinken während sie laufen.
Die `dynamicParams` Einstellung
Hier ein wichtiger Hinweis: Halte dynamicParams = true, um ISR die Generierung von Seiten außerhalb von generateStaticParams zu erlauben. Es klingt offensichtlich, aber du würdest erstaunt sein, wie oft das übersehen wird.
export const dynamicParams = true; // On-Demand Generierung erlauben
Parallele Route-Segmente
Für Seiten mit Kategorien und Standorten nutzten wir parallele Route-Segmente, damit Filter und Listing-Gitter unabhängig laden konnten:
// app/directory/[category]/layout.tsx
export default function CategoryLayout({
children,
filters,
}: {
children: React.ReactNode;
filters: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[280px_1fr] gap-6">
<aside>{filters}</aside>
<main>{children}</main>
</div>
);
}
Das bedeutet, deine Filter können separat gecacht werden. Ändere einen Filter, und nur das Listing-Gitter wird neu gerendert. Schnell!
Datenbank und Suchschicht
PostgreSQL auf Neon
Wir wählten Neon wegen seiner serverlosen Vorzüge wie Skalierung und Preview-Branches. Die Art von Dingen, die uns das Leben erleichterten.
Unsere Listings-Tabelle ist unkompliziert, vertraut aber stark auf Indexierung:
CREATE INDEX idx_listings_category ON listings(category_id);
CREATE INDEX idx_listings_location ON listings USING GIST(location);
CREATE INDEX idx_listings_rating ON listings(avg_rating DESC);
CREATE INDEX idx_listings_slug ON listings(slug);
CREATE INDEX idx_listings_status_tier ON listings(status, tier);
Warum der GiST-Index auf location? Es geht um präzise Geodaten-Abfragen. "Coffeeshops in meiner Nähe" ist nicht nur Gerede; es ist eine echte Berechnung.
Meilisearch für Suche
Wenn deine Liste wie unsere anschwillt, reicht PostgreSQLs Textsuche nicht aus, und da kommt Meilisearch ins Spiel. Es schlug Algolia für uns, hauptsächlich beim Preis ($30/Monat vs. $200+) und seiner beeindruckenden Tippfehlertoleranz.
// lib/search.ts
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({
host: process.env.MEILISEARCH_HOST!,
apiKey: process.env.MEILISEARCH_API_KEY!,
});
export async function searchListings(query: string, filters: FilterParams) {
const index = client.index('listings');
return index.search(query, {
filter: buildFilterString(filters),
facets: ['category', 'city', 'priceRange', 'rating'],
limit: 24,
offset: filters.page * 24,
attributesToHighlight: ['name', 'description'],
});
}
Alle fünf Minuten synchronisieren sich Einträge mit einem Job. Wir führen wöchentlich eine vollständige Neindizierung durch, nur um sicherzugehen. Besser sicher als sorry, richtig?
SEO im großen Maßstab: Sitemaps, strukturierte Daten und Crawl-Budget
Für eine Plattform mit 137.000 Seiten ist SEO nicht nur nice-to-have; es ist lebenswichtig. Hier ist, wie wir es geschafft haben:
Dynamische Sitemaps
Du kannst nicht 137.000 URLs in eine einzelne Sitemap-Datei packen. Das Limit liegt laut Spezifikation bei 50.000 URLs. Also, was tun wir? Wir generieren einen Sitemap-Index, der auf segmentierte Teile zeigt:
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Dies generiert den Sitemap-Index
const totalListings = await db.listing.count({ where: { status: 'active' } });
const sitemapCount = Math.ceil(totalListings / 10000);
const sitemaps = [];
for (let i = 0; i < sitemapCount; i++) {
sitemaps.push({
url: `${process.env.NEXT_PUBLIC_URL}/sitemap/${i}.xml`,
lastModified: new Date(),
});
}
return sitemaps;
}
Segmentierte Sitemaps enthalten 10.000 Einträge, mit Zeitstempel. Google crawlt täglich etwa 8.000-12.000 Seiten.
Strukturierte Daten
Jede Listing-Seite ist vollgepackt mit LocalBusiness-Schema-Markup:
function generateStructuredData(listing: Listing) {
return {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: listing.name,
description: listing.description,
address: {
'@type': 'PostalAddress',
streetAddress: listing.address,
addressLocality: listing.city,
addressRegion: listing.state,
postalCode: listing.zip,
},
aggregateRating: listing.reviewCount > 0 ? {
'@type': 'AggregateRating',
ratingValue: listing.avgRating,
reviewCount: listing.reviewCount,
} : undefined,
geo: {
'@type': 'GeoCoordinates',
latitude: listing.lat,
longitude: listing.lng,
},
};
}
Diese Art von strukturierten Daten turboladete unsere Rankings, wobei Google solch präzise Informationen bevorzugt.
Leistungs-Benchmarks
Echte Metriken von unsere Live-Seite ab früh 2025:
| Metrik | Wert | Ziel |
|---|---|---|
| Largest Contentful Paint (LCP) | 1,1s (p75) | < 2,5s |
| First Input Delay (FID) | 12ms (p75) | < 100ms |
| Cumulative Layout Shift (CLS) | 0,02 (p75) | < 0,1 |
| Time to First Byte (TTFB) | 85ms (gecacht) / 190ms (kalter ISR) | < 200ms |
| Lighthouse Performance Score | 94-98 | > 90 |
| Build-Zeit | 3 Min 22 Sek | < 5 Min |
| Cache Hit Rate | 94,7% | > 90% |
Diese hohe Cache-Hit-Rate? Ja, satte 94,7% unserer Seiten kommen direkt aus Vercels Edge CDN – keine zusätzlichen Berechnungen nötig. Ein Gewinn-Gewinn für Geschwindigkeit und Kosten.
Kostenaufschlüsselung auf Vercel
Kommen wir zu Dollar und Cent. Wer liebt nicht ein gutes Schnäppchen?
| Service | Monatliche Kosten (2025) | Notizen |
|---|---|---|
| Vercel Pro | $20/Sitz | Für Pro-Level-Features und Limits |
| Vercel Bandbreite | ~$55 | ~600GB/Monat mit ISR-Caching |
| Vercel Serverless-Funktionen | ~$40 | Für ISR-Arbeit + API-Sachen |
| Neon PostgreSQL | $19 (Scale-Plan) | 10GB Speicher, skalierbare Compute |
| Meilisearch Cloud | $30 | 500K Docs, dedizierte Instanz |
| Upstash Redis | $10 | 10K Befehle/Tag im Durchschnitt |
| Cloudinary | $25 | Bildspeicher und Transformationen |
| Gesamt | ~$199/Monat | Für 137K Seiten, ~200K monatliche Besucher |
Unter $200/Monat, um ein Monster mit 137.000 Seiten zu betreiben. Gegenüber einem traditionellen Server-Setup? Du würdest Geld bluten auf VMs, verwalteten DBs, CDNs und einem vollzeitigen DevOps, um alles zu babysitten.
Wenn du in diesem Maßstab spielst und einen Chat möchtest, kontaktiere uns oder schau dir unsere Preisgestaltung an.
Fehler, die wir gemacht haben, und was wir ändern würden
Fehler 1: On-Demand Revalidation nicht von Anfang an einrichten
Wir verließen uns anfangs nur auf zeitgesteuerte Revalidierung. Lass mich dir sagen, schlechter Zug. Eintrag-Inhaber würden ihre Informationen tweaken und sofort checken. Alte Daten sehen? Kein Vertrauensbooster. Revalidierung musste MVP sein.
Fehler 2: Sitemap-Komplexität unterschätzen
Unser erster Versuch einer Sitemap jamte alles in eine Serverless-Funktion. Cue Timeouts. Vercel gibt dir 10 Sekunden (60 auf Pro) vor dem Timeout. Wir haben gelernt. Segmentiere diese Dinger.
Fehler 3: Bildoptimierungs-Kosten
Ursprünglich handhabte Vercel alle Listing-Foto-Optimierungen. Eine verrückte Menge an Bildern bedeutete wilde Kosten. Wir teilten diese Aufgabe mit Cloudinary auf und behielten Vercels Magie für UI-Must-Haves.
Fehler 4: React Server Components nicht aggressiv genug nutzen
Einige anfängliche Seiten waren mit zu vielen 'use client'-Befehlen vollgepackt. Resultat? Zu viel JavaScript versandt. Der Fokus auf Server Components machte unser JavaScript-Bundle leicht wie eine Feder (62% Reduktion!).
Was wir beim nächsten Mal anders machen würden
Beim nächsten Mal würden wir absolut Next.js mit etwas wie Payload CMS von Anfang an koppeln, anstatt von Grund auf einen Admin-Panel zu hacken. Was für eine Zeitersparnis das gewesen wäre!
Wir würden auch Vercels neuesten unstable_cache (oder einfach cache jetzt) für Abfrageergebnisse über das Standard-ISR-Caching hinaus sorgfältig in Betracht ziehen.
Häufig gestellte Fragen
Kann Next.js ISR wirklich Hundertausende von Seiten handhaben?
Absolut. Wir haben es vorgemacht. Generiere deine Top-Traffic-Seiten vorab (üblicherweise 1-5%) mit generateStaticParams und lass ISR um den Rest kümmern. Vercels Edge übernimmt von da an und sorgt für schnelle Ladezeiten global.
Wie viel kostet es, eine große Verzeichnisseite auf Vercel zu betreiben?
Für uns sind es etwa $199/Monat für 137K Listings mit 200.000 monatlichen Besuchern. Die Kosten werden sicherlich variieren, aber wenn du das süße Caching-Tempo triffst, kann ISR dich großartig sparen.
Was ist der Unterschied zwischen ISR und SSR für Verzeichnisseiten?
ISR generiert Seiten einmal pro Revalidierungs-Intervall und cached sie, während SSR Seiten bei jeder Anfrage von Grund auf neu generiert. ISR ist effizienter für Einträge, wo sich Daten nicht jede Minute ändern.
Wie handhabst du die Suche auf einer statisch generierten Verzeichnisseite?
Such-Interaktionen gehen direkt zu Meilisearch, mit API-Aufrufen zum Abdecken. Such-Ergebnisse werden Client-seitig gerendert, während Listing-Seiten ISR-gestützt sind. Es ist die beste Mischung aus statisch und dynamisch.
Welches Revalidierungs-Intervall sollte ich für ISR auf einer Verzeichnisseite verwenden?
Hängt von der Änderungshäufigkeit ab. Wir verwenden einen gestuften Ansatz: 10 Min für Premiums, 1 Stunde für Standards und 24 Stunden für ruhigere Einträge. Würze mit On-Demand Revalidation für sofortige Änderungen.
Wie generierst du Sitemaps für 137.000 Seiten ohne zu timeout?
Segmentierung ist dein Freund. Teile sie in Chunks von 10.000 auf. Leite sie durch einen Sitemap-Index. Jeder Chunk sollte bequem innerhalb der Timeout-Limits bleiben.
Ist Next.js das beste Framework zum Erstellen von Verzeichnisplattformen?
Ja, für Heavy Hitter – besonders mit ISR. Für ultra-einfache, selten wechselnde Listen? Astro kann eine leichte Option sein. Wir haben beide erstellt; die Wahl hängt von deiner Workload und deinen Bedürfnissen ab.
Wie verhinderst du, dass veraltete Daten die Benutzererfahrung mit ISR beeinträchtigen?
Das Mischen von zeitgestützter und On-Demand Revalidierung hilft. Paare das mit Client-seitiger SWR oder React Query für ultra-frische Daten. ISR speist deine Shell, während Real-Time selektiv scheint.