Irgendwann in der zweiten Hälfte von 2022 überredete ich mich selbst, dass der Bau eines Web-Hosting-Verzeichnisses geradlinig sein würde. Daten aggregieren, Seiten generieren, ranken, monetarisieren. Sauber. Ich hatte programmgesteuerte SEO-Projekte zuvor gemacht — ein lokalisiertes Immobilien-Tool für einen UK-Kunden, eine SaaS-Vergleichsseite, die 40k monatliche Besuche erreichte — also ging ich davon aus, dass HostList ein Sechswochenprojekt wäre. Es dauerte näher an sieben Monaten. Und es hätte fast ein paar Dinge kaputt gemacht: meinen Schlafrhythmus, das Selbstvertrauen eines meiner Junior-Entwickler und eine monatliche Vercel-Rechnung von 180 £, die ich nicht eingeplant hatte.
Das ist die Post-Mortem-Analyse. Die echte, nicht die LinkedIn-Version.
---
Das Briefing, das ich mir selbst schrieb
HostList sollte einfach sein. Ein Verzeichnis von Web-Hosting-Providern — Shared, VPS, Dedicated, Managed WordPress — mit einzelnen Seiten für jeden Provider, Vergleichsseiten, Kategorieseiten und standortbasierte Seiten (z. B. „bestes Hosting in Deutschland"). Rechnen wir durch: ~400 Provider × mehrere Seitentypen × 20+ Filterkombinationen. Man kommt schneller zu 25.000 Seiten, als man denkt.
Ich habe mich für Next.js entschieden, fast ohne darüber nachzudenken. Bei Seahawk nutzen wir es für die meisten größeren React-basierten Projekte. Das Ökosystem ist ausgereift, getStaticProps und getStaticPaths machen Sinn für SEO-lastige statische Generierung, und ich persönlich finde das dateibasierte Routing leichter zu durchschauen als Remix oder Gatsby in diesem Maßstab.Next.jsalmost without thinking about it. We use it at Seahawk for most of our bigger React-based builds. The ecosystem is mature,getStaticPropsandgetStaticPathsmake sense for SEO-heavy static generation, and I personally find the file-based routing easier to reason about than Remix or Gatsby at this scale.
Die erste echte Entscheidung war die Datenschicht. Ein Headless-CMS habe ich ziemlich schnell ausgeschlossen — ich wollte nicht Contentful-Preise für 25.000 Einträge zahlen, und ich traute einem CMS nicht zu, große programmatische Writes sauber zu handhaben. Wir landeten bei einer Postgres-Datenbank auf Supabase mit einer leichtgewichtigen Next.js-API-Schicht davor. Das Teil hat eigentlich funktioniert. Fast alles andere wurde kompliziert.Supabase, with a lightweight Next.js API layer sitting in front of it. That part actually worked fine. It's almost everything else that got complicated.
---
Statische Generierung im großen Maßstab: Wofür dich niemand warnt
Das Problem mit getStaticPaths bei 25.000 Routes ist: Es funktioniert. Technisch gesehen. Aber deine Build-Zeiten werden dich an deinen Lebensentscheidungen zweifeln lassen.getStaticPathswith 25,000 routes. It works. Technically. But your build times will make you question your life choices.
Unser erstes vollständiges Build hat 4 Stunden und 47 Minuten gedauert. Auf Vercel. Das ist genau das Zeug, das dir eine Abrechnung um 2 Uhr morgens auf den Tisch bringt, wenn du nicht vorsichtig mit deinen Plan-Limits umgekst. Ich starrte auf diese Slack-Benachrichtigung von meinem Handy und ernsthaft erwog ich, einfach WordPress zu nutzen.
Die `fallback: 'blocking'`-Falle
Mein erster Instinkt war, alles vorab zu rendern. Jede Seite, jede Kombination. Schlechte Idee — und nicht aus dem Grund, vor dem die meisten Tutorials dich warnen (das ist normalerweise nur „es dauert eine Weile"). Das echte Problem ist Cache-Invalidierung. Wenn ein Hosting-Anbieter seine Preise aktualisiert (und das tut er ständig), musst du die betroffenen Seiten neu erstellen. Wenn alles statisch vorgerendert ist ohne ISR, triggert du vollständige Rebuilds für Datenänderungen, die vielleicht 30 Seiten von 25.000 betreffen.cache invalidation. When a hosting provider updates their pricing (and they do, constantly), you need to rebuild affected pages. If everything is statically pre-rendered with no ISR, you're triggering full rebuilds for data changes that affect maybe 30 pages out of 25,000.
Ich bin zu Incremental Static Regeneration gewechselt mit einem revalidate von 86400 Sekunden (24 Stunden) für die meisten Seiten und 3600 Sekunden für preislastige Provider-Seiten. Das war die einzelne größte Verbesserung der Lebensqualität im ganzen Projekt. Build-Zeiten fielen auf unter 40 Minuten, weil wir nur die top ~2.000 Seiten nach Traffic-Priorität vorrenderten und den Rest on-demand mit fallback: 'blocking' generieren ließen.Incremental Static Regenerationwith arevalidateof 86400 seconds (24 hours) for most pages, and 3600 seconds for pricing-heavy provider pages. This was the single biggest quality-of-life improvement in the entire project. Build times dropped to under 40 minutes because we were only pre-rendering the top ~2,000 pages by traffic priority and letting the rest generate on-demand withfallback: 'blocking'.
Das Route-Tree aufteilen
Eine Sache, die ich anders machen würde, und das sage ich jedem Dev bei Seahawk, der jetzt an einem großen programmgesteuerten Projekt arbeitet: Teile deinen Route-Tree früh auf. Lass nicht eine monolithische getStaticPaths-Funktion versuchen, 25.000 Slugs zurückzugeben. Wir haben unsere in folgende aufgeteilt:getStaticPathsfunction trying to return 25,000 slugs. We broke ours into:
/providers/[slug] — einzelne Provider-Seiten (~400)— individual provider pages (~400)/compare/[slugA]-vs-[slugB] — Head-to-Head-Vergleichsseiten (~8.000)— head-to-head comparison pages (~8,000)/category/[type] — Kategorie-Landingpages (~40)— category landing pages (~40)/location/[country]/[type] — Geo × Kategorie-Kombinationen (~16.000+)— geo × category combinations (~16,000+)/best/[use-case] — kurierte Listenseiten (~600)— curated list pages (~600)
Jede Route-Gruppe hat ihre eigene Revalidierungs-Kadenz, ihre eigene Datenbeschaffungslogik und, das ist entscheidend, ihre eigene Build-Priorität. Die Location-Seiten sind fast vollständig On-Demand. Die Provider-Seiten werden immer vorgerendert. Saubere Trennung.
---
Das Data-Pipeline-Chaos (und wie wir es behoben)
Anfang 2023 habe ich den Fehler gemacht, die Datenbeschaffungsseite von HostList.io zu locker zu bauen. Wir hatten ein Scraping-Skript (geschrieben in Python, mit BeautifulSoup und einem rotierenden Proxy-Pool von Webshare), ein manuelles Google Sheet für Korrektionen und eine Supabase-Tabelle. Drei Quellen der Wahrheit. Keine von ihnen sprach richtig miteinander.
Ein Junior Dev — guter Kerl, gerade aus einem Bootcamp raus — hat drei Wochen damit verbracht, ein Sync-Skript zwischen dem Sheet und Supabase zu warten, das jedes Mal brach, wenn sich ein Spaltenname änderte. Ich hätte das Sheet in Woche eins abschalten und eine ordentliche interne Admin-UI bauen sollen. Am Ende haben wir das getan, mit Next.js API Routes und einem Retool Dashboard auf der Seite, aber wir haben wahrscheinlich 60 Engineering Hours dafür aufgewendet.
Die Lösung: eine einzige Quelle der Wahrheit, immer. Die Datenbank ist kanonisch. Alles schreibt in die Datenbank. Die Admin-UI liest aus der Datenbank und schreibt in sie. Der Scraper schreibt in die Datenbank. Klingt offensichtlich. Im Nachhinein ist es das immer.one source of truth, always. The database is canonical. Everything writes to the database. The admin UI reads from and writes to the database. The scraper writes to the database. Sounds obvious. It always does, in hindsight.
Daten in Bewegung im großen Maßstab
Für ein Verzeichnis dieser Größe ist Datenaktualität genauso ein SEO-Problem wie ein UX-Problem. Google merkt es, wenn Preistabellen £2.99/month für einen Plan anzeigen, der seit acht Monaten £5.99 kostet. Wir haben eingerichtet:
- Ein wöchentlicher Scrape-Job auf einem Railway Cron (günstig, zuverlässig, erfordert keinen dedizierten Server)
- Einen Supabase-Datenbank-Webhook, der ausgelöst wird, wenn eine price_updated_at Spalte sich ändert und auf einen Next.js Revalidierungspunkt trifft
price_updated_atcolumn changes, hitting a Next.js revalidation endpoint - Manuelle Überschreibe-Flags in Retool für die ~30 Provider, deren Websites aktiv Scraper blockieren
Dieser Revalidierungspunkt — /api/revalidate?secret=TOKEN&path=/providers/siteground — ist ein Standard-Next.js-Feature, aber das Verdrahten mit einem Datenbank-Webhook erforderte etwas Installationsarbeit. Jede Minute wert./api/revalidate?secret=TOKEN&path=/providers/siteground— is a stock Next.js feature, but wiring it to a database webhook took a bit of plumbing. Worth every minute.
---
SEO-Architektur: Was das Nadelöhr wirklich bewegte
Ich habe genug Content-Sites aufgebaut, um zu wissen, dass 25.000 Seiten nicht dasselbe ist wie 25.000 Seiten, die ranken. Die Vergleichsseiten waren die Falle. Wir haben jede mögliche A-gegen-B-Kombination für unsere ~400 Provider generiert, was uns grob 79.800 theoretische Paarungen gab. Wir haben ~8.000 davon gebaut. Und die meisten davon waren ehrlich gesagt dünn.
Ehrliche Geständnis: Ich bin gierig geworden. Die SEO-Logik war solide — "SiteGround vs Bluehost" hat echtes Suchvolumen, der Long-Tail von Vergleichsabfragen ist riesig — aber wir haben nicht genug einzigartige Inhalte pro Seite erstellt, um die Existenz jeder einzelnen zu rechtfertigen. Google hat angefangen, den Vergleichsbereich zu crawlen und hat offensichtlich entschieden, dass es seine Zeit nicht wert ist. Googles eigene Anleitung zu dünnen Inhalten ist deutlich darüber, und ich hätte früher deutlicher mit mir selbst sein sollen.Google's own guidance on thin contentis blunt about this, and I should have been blunter with myself earlier.
Was wir taten, um uns zu erholen
Wir haben ausgemistet. Die Vergleichsseiten von ~8.000 auf ~1.200 reduziert — nur Paarungen mit nachweisbarem Suchvolumen (in Ahrefs überprüft, mindestens 50 monatliche Suchen weltweit). Dann haben wir die verbleibenden Seiten angereichert mit:
- Dynamische "wofür es am besten geeignet ist"-Abschnitte, die aus strukturierten Provider-Daten gezogen werden
- Echte Uptime-Daten (wir haben uns mit einer Uptime-API eines Drittanbieters integriert)
- Benutzer-Review-Zusammenfassungen aus Trustpilot-Daten, wo verfügbar
Das Ergebnis waren 1.200 Seiten, die tatsächlich nützlich waren, statt 8.000 Seiten, die es nicht waren. Der organische Traffic zum Vergleichsbereich stieg über die folgenden drei Monate um 340 %. Kontraintuitiv, bis es nicht mehr so ist.
Interne Verlinkung in diesem Maßstab
Mit 25.000 Seiten kann interne Verlinkung nicht manuell sein. Wir haben eine Related-Pages-Komponente gebaut, die Supabase zur Build-Zeit (in getStaticProps) abfragt und die fünf relevantesten angrenzenden Seiten basierend auf Kategorie- und Standort-Überschneidungen zurückgibt. Keine redaktionelle Einmischung nötig. Es ist nicht perfekt — gelegentlich verlinkt eine VPS-Hosting-Seite auf etwas etwas Schräges — aber es ist zu 90 % richtig, und es bedeutete, dass jede Seite vom ersten Tag an kontextuell relevante interne Links hatte.getStaticProps) and returns the five most relevant adjacent pages based on category and location overlap. No editorial intervention needed. It's not perfect — occasionally a VPS hosting page links to something a bit sideways — but it's 90% right, and it meant every page had contextually relevant internal links from day one.
---
Performance: Der Teil, der dich demütigt
Man würde denken, dass statische Generierung Performance einfach macht. Und auf konzeptioneller Ebene tut sie das auch — vorgeneriertes HTML, edge-cached auf Vercels CDN, kein Server-Rendering-Overhead. Aber 25.000 Seiten bedeuten 25.000 Möglichkeiten, eine schlechte Entscheidung über deinen Component Tree zu treffen.
Unser größtes Performance-Problem war die Provider-Vergleichstabelle. Es war eine schwere clientseitige React-Komponente — viel State, viel bedingtes Rendering, verwendet auf Provider-Seiten und Vergleichsseiten. Auf Mobilgeräten verursachte sie einen Largest Contentful Paint von etwa 4,8 Sekunden. Schlecht. Wirklich schlecht für eine Website, auf der der primäre Traffic von Menschen kommt, die sich mitten in einer Kaufentscheidung befinden.Largest Contentful Paintof around 4.8 seconds. Bad. Really bad for a site where the primary traffic is people mid-decision on a purchase.
Wir haben sie als server-gerendertes statisches Table mit einer dünnen React-Hydration-Schicht für die interaktiven Filter-Teile umgebaut. Das LCP fiel auf 1,9 Sekunden. Das ist keine Magie — es ist einfach das Langweilige richtig gemacht.
Das Image-Problem
Jeder Provider hat ein Logo. 400 Logos, plus Screenshots, UI-Vorschaubilder, Feature-Icons. Wir haben den Fehler gemacht, diese die ersten zwei Monate auf Vercels integrierte Image-Optimierung zu hosten. Die Bandbreitenkosten waren leise entsetzlich. Alles auf Cloudflare R2 mit einer benutzerdefinierten Domain verschoben, Vercels Rechnung von £180/Monat auf £40/Monat gesenkt. Wenn du irgendetwas Bildlastiges baust, schau dir Cloudflare R2 früh an — der kostenlose Egress ist im großen Maßstab wirklich nützlich.Cloudflare R2early — the free egress is genuinely useful at scale.
---
Wie die Build-Pipeline jetzt eigentlich aussieht
Für alle, die sich das konkrete Bild ansehen wollen:
- Datenerfassung — Python-Scraper auf Railway-Cron-Job, schreibt in Supabase Postgres— Python scraper on a Railway cron job, writes to Supabase Postgres
- Admin-Layer — Retool-Dashboard für manuelle Änderungen, Korrektionen und Provider-Flags— Retool dashboard for manual edits, corrections, and provider flags
- Next.js-App — Pages Router (wir haben angefangen, bevor App Router stabil genug war), deployed auf Vercel— Pages router (we started before App Router was stable enough to trust), deployed on Vercel
- ISR + On-Demand-Revalidierung — die oberen ~2.000 Seiten vorgefertigt, Rest on-demand, alle mit 24h-Revalidierung— top ~2,000 pages pre-built, rest on-demand, all with 24h revalidation
- Bilder — Cloudflare R2, bereitgestellt über eine benutzerdefinierte Subdomain mit Cloudflare CDN davor— Cloudflare R2, served via a custom subdomain with Cloudflare CDN in front
- Analytics — Plausible für datenschutzfreundliche Traffic-Daten, Ahrefs für Ranking-Tracking— Plausible for privacy-friendly traffic data, Ahrefs for ranking tracking
- Verfügbarkeitsüberwachung — BetterUptime überwacht die fünf traffic-stärksten Seitentypen— BetterUptime watching the five most traffic-heavy page types
Es ist nicht glamourös. Es ist auch weitgehend langweilig zu warten, was genau das ist, was man sich von einer Infrastruktur wünscht, die man drei Jahre lang laufen lässt.
---
Ehrliche Fehler, nummeriert
- Ich bin zu breit gestartet. 25.000 Seiten waren immer das Ziel, aber ich hätte mit 500 hochwertigen Seiten starten und dann expandieren sollen. Stattdessen bin ich mit allem gestartet und hatte die ersten vier Monate ein Google-Crawl-Budget-Problem.25,000 pages was always the goal, but I should have launched with 500 high-quality pages and expanded. Instead I launched with everything and had a Google crawl budget problem for the first four months.
- Habe die Revalidierung von Tag eins an nicht richtig eingerichtet. Wir haben zwei Monate mit vollständigen Rebuilds verschwendet, die ISR unnötig gemacht hätte.We wasted two months on full rebuilds that ISR would have made unnecessary.
- Habe das Google Sheet behalten. Eine einzige Quelle der Wahrheit hätte von Woche eins an nicht verhandelbar sein sollen.Single source of truth should have been non-negotiable from week one.
- Die Qualität von Vergleichsseiten unterschätzt. Volumen ist keine Strategie.Volume is not a strategy.
- Vercel Image Optimization viel zu lange verwendet. Bin sechs Wochen später als nötig zu R2 gewechselt.Moved to R2 six weeks later than we should have.
- Den Route-Tree nicht früh genug aufgeteilt. Schnelle und langsame Routes im selben getStaticPaths Call gemischt und mich dann gewundert, warum Builds langsam waren.Mixed fast and slow routes in the same
getStaticPathscall and then wondered why builds were slow.
Jede einzelne dieser Entscheidungen schien damals vernünftig. Das ist der Teil, den Tutorials nicht erfassen – schlechte architektonische Entscheidungen haben normalerweise gut klingende Begründungen, wenn man sie trifft.
---
FAQ
Wie lange hat der initiale Build gedauert, um live zu gehen?
Sieben Monate vom ersten Commit bis zu einer Version, die ich komfortabel v1 nennen konnte. Die erste grobe öffentliche Version war nach etwa vier Monaten live, hatte aber ernsthafte Probleme mit dünnem Inhalt und der Vergleichsbereich war größtenteils nutzlos. Ich würde sagen vier Monate bis „technisch live" und weitere drei bis „eigentlich gut".
Würdest du den App Router verwenden, wenn du heute anfangen würdest?
Wahrscheinlich ja, für neue Projekte ab Ende 2023. Die Server Components des App Routers würden sich eigentlich gut für diese Art der datenintensiven Seitengenerierung eignen. Aber eine bestehende 25.000-Seiten-Pages-Router-App zu migrieren ist kein Projekt, das ich in absehbarer Zeit in Angriff nehme. Der Pages Router funktioniert noch, und „funktioniert" wird unterschätzt.
Wie gehst du mit Providern um, die aus dem Geschäft gehen oder ihr Angebot erheblich ändern?
Wir haben ein Status-Flag in der Datenbank — active, deprecated, redirected. Deprecated Provider bekommen eine schlanke Archivseite statt vollständiger Entfernung, was Backlinks bewahrt. Redirected Provider (z.B. wenn ein Host einen anderen übernimmt) bekommen einen 301 über die Next.js-Konfiguration redirects in next.config.js. Wir überprüfen die Status-Flags monatlich.statusflag in the database —active,deprecated,redirected. Deprecated providers get a slim archive page rather than a full removal, which preserves any backlinks. Redirected providers (e.g. when one host acquires another) get a 301 handled via the Next.jsredirectsconfig innext.config.js. We review the status flags monthly.
Was würdest du statt Next.js verwenden, wenn du das noch mal machen würdest?
Ich weiß ehrlich gesagt nicht. Astro ist interessant für überwiegend statische Content-Seiten, und ich spiele damit bei einem kleineren Projekt herum. Aber Next.js gab uns die Flexibilität, sowohl statische als auch dynamische Sections in derselben Codebasis zu haben, das war wichtig. Für ein rein statisches Verzeichnis ohne interaktive Features könnte Astro schneller zu bauen und billiger zu betreiben sein. Frag mich in einem Jahr nochmal.
Wie stoppst du Scraper davon ab, das ganze Verzeichnis zu kopieren?
Ehrlich gesagt? Das kannst du nicht, vollständig. Wir rate-limiten die API Routes, nutzen Cloudflare's Bot Management auf dem Frontend und rotieren einen Teil der strukturierten Daten, damit gescrapte Kopien schnell veralten. Aber wenn jemand ein öffentliches Verzeichnis klonen möchte, wird er einen Weg finden. Der Burggraben ist Datenfreshness und UX-Qualität, keine technische Verschleierung.
---
Abschließender Gedanke
HostList ist kein durchschlagender Erfolg. Es generiert Einnahmen — Affiliate-Provisionen, ein paar direkte Werbedeals — und rankt einigermaßen gut für vielleicht 600 der Begriffe, die ich ursprünglich angepeilt habe. Das ist in Ordnung. Es war ein Lernprojekt, das nebenbei auch noch Umsatz generiert, und das ist die beste Variante.
Falls du darüber nachdenkst, eine großangelegte programmatische SEO-Website auf Next.js zu bauen, mein ehrlicher Rat ist dieser: Mach es. Es ist wirklich ein guter Stack für diese Aufgabe. Aber baue weniger, als du denkst, dass du brauchst, baue es besser, als du denkst, dass du Zeit für hast, und klär deine Datenarchitektur, bevor du eine einzige Page-Template schreibst.
Die Technologie ist der einfache Teil. Das ist sie immer.
