Ende 2022 redete ich mir ein, dass der Aufbau eines Web-Hosting-Verzeichnisses einfach sein würde. Daten aggregieren, Seiten generieren, ranken, monetarisieren. Sauber. Ich hatte vorher schon programmatische SEO-Projekte gemacht – ein lokalisiertes Immobilientool für einen UK-Kunden, eine SaaS-Vergleichsseite, die 40k monatliche Besucher erreichte – also dachte ich, HostList wäre ein Sechswochenprojekt. Es dauerte näher an sieben Monaten. Und es hätte fast ein paar Dinge zerstört: meinen Schlafrhythmus, das Selbstvertrauen eines meiner Junior-Entwickler und eine £180-monatliche Vercel-Rechnung, die ich nicht eingeplant hatte.Vercel bill I hadn't budgeted for.
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-Anbietern – Shared Hosting, VPS, Dedicated Server, verwaltetes WordPress – mit einzelnen Seiten für jeden Anbieter, Vergleichsseiten, Kategorieseiten und ortsbezogenen Seiten (z.B. „bestes Hosting in Deutschland"). Rechne die Zahlen: ~400 Anbieter × mehrere Seitentypen × 20+ Filterkombinationen. Man landet schneller bei 25.000 Seiten, als man denkt.WordPress -- with individual pages for each provider, comparison pages, category pages, and location-based pages (e.g. "best hosting in Germany"). Run the maths: ~400 providers × several page types × 20+ filter combinations. You get to 25,000 pages faster than you'd think.
Ich habe mich für Next.js entschieden, ohne viel darüber nachzudenken. Wir nutzen es bei Seahawk für die meisten unserer größeren React-basierten Projekte. Das Ökosystem ist ausgereift, getStaticProps und getStaticPaths sind sinnvoll für SEO-lastige statische Generierung, und ich persönlich finde das dateibasierte Routing einfacher zu durchschauen als Remix oder Gatsby in diesem Maßstab.Next.js almost without thinking about it. We use it at Seahawk for most of our bigger React-based builds. The ecosystem is mature,getStaticProps and getStaticPaths make 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. Ich habe ein Headless CMS ziemlich schnell ausgeschlossen – ich wollte nicht Contentful-Raten für 25.000 Einträge zahlen, und ich traute einem CMS nicht, Massen-Programmatic-Writes sauber zu handhaben. Wir landeten bei einer Postgres-Datenbank auf Supabase, mit einer leichtgewichtigen Next.js-API-Schicht davor. Das hat tatsächlich gut funktioniert. Fast alles andere ist kompliziert geworden.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 und 25.000 Routes: Es funktioniert. Technisch gesehen. Aber deine Build-Zeiten werden dich deine Lebensentscheidungen überdenken lassen.getStaticPaths with 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 vorzurendern. Jede Seite, jede Kombination. Schlechte Idee – und nicht aus dem Grund, vor dem die meisten Tutorials warnen (das ist normalerweise nur „es dauert eine Weile"). Das echte Problem ist Cache-Invalidierung. Wenn ein Hosting-Anbieter seine Preise aktualisiert (und das tun sie ständig), musst du betroffene Seiten neu erstellen. Wenn alles statisch vorgefertigt ist ohne ISR, löst du Full Rebuilds für Datenänderungen aus, 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 mit einem revalidate von 86400 Sekunden (24 Stunden) für die meisten Seiten und 3600 Sekunden für preisintensive Provider-Seiten übergegangen. Das war die einzelne größte Verbesserung der Lebensqualität im ganzen Projekt. Build-Zeiten fielen unter 40 Minuten, weil wir nur die Top ~2.000 Seiten nach Traffic-Priorität vorab renderten und den Rest mit fallback: 'blocking' on-demand generieren ließen.Incremental Static Regeneration with a revalidate of 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 with fallback: 'blocking'.
Das Route-Tree aufteilen
Eine Sache, die ich anders machen würde, und die ich jedem Entwickler bei Seahawk sage, der ein großes programmatisches Projekt anfasst: Teile deinen Route-Baum früh auf. Habe keine monolithische getStaticPaths-Funktion, die versucht, 25.000 Slugs zurückzugeben. Wir teilten unsere auf in:getStaticPaths function trying to return 25,000 slugs. We broke ours into:
/providers/[slug] – einzelne Anbieterseiten (~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] – kuratierte 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 Developer – guter Kerl, gerade aus einem Bootcamp – hat drei Wochen lang ein Sync-Skript zwischen Sheet und Supabase gepflegt, das bei jeder Spaltenumbennung kaputt ging. Ich hätte das Sheet in Woche eins killen und eine richtige interne Admin-UI bauen sollen. Am Ende haben wir es gemacht – mit Next.js API Routes und einem Retool Dashboard daneben – aber wir haben wahrscheinlich 60 Engineering-Stunden dafür gebrannt.
Die Lösung: eine einzige Quelle der Wahrheit, immer. Die Datenbank ist kanonisch. Alles schreibt in die Datenbank. Die Admin-UI liest aus und schreibt in die Datenbank. Der Scraper schreibt in die Datenbank. Klingt offensichtlich. Im Nachhinein tut 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 auslöst, wenn sich eine price_updated_at-Spalte ändert, und einen Next.js-Revalidierungs-Endpunkt 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
Jener Revalidation-Endpoint -- /api/revalidate?secret=TOKEN&path=/providers/siteground -- ist ein Standard Next.js Feature, aber das Verkabeln mit einem Database-Webhook brauchte ein bisschen Klempnerarbeit. Jede Minute hat sich gelohnt./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.
Ehrlich gestanden: Ich bin gierig geworden. Die SEO-Logik war solide – "SiteGround vs Bluehost" hat echtes Suchvolumen, die Long-Tail von Vergleichsabfragen ist riesig – aber wir haben nicht genug einzigartigen Content pro Seite gebaut, um die Existenz von jeder einzelnen zu rechtfertigen. Google hat angefangen, den Vergleichsbereich zu crawlen und hat das klar als nicht der Mühe wert eingestuft. Googles eigene Richtlinien zu Thin Content sind da direkt, und ich hätte früher direkter mit mir selbst sein sollen.Google's own guidance on thin content is blunt about this, and I should have been blunter with myself earlier.
Was wir taten, um uns zu erholen
Wir haben aufgeräumt. Die Vergleichsseiten von ~8.000 auf ~1.200 reduziert – nur Paare mit messbarem Suchvolumen (verifiziert in Ahrefs, minimum 50 monatliche Suchen global). 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 Internal Linking nicht manuell sein. Wir haben eine Related-Pages-Komponente gebaut, die zur Build-Zeit Supabase abfragt (in getStaticProps) und gibt die fünf relevantesten benachbarten Seiten basierend auf Kategorie- und Location-Überlappung zurück. Keine redaktionelle Intervention nötig. Es ist nicht perfekt – gelegentlich verlinkt eine VPS-Hosting-Seite auf etwas ein bisschen Sideways – aber es ist zu 90% richtig, und das 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 könnte meinen, statische Generierung würde Performance einfach machen. Und auf konzeptioneller Ebene macht es das – pre-gerendeter HTML, edge-cached auf Vercel CDN, kein Server-Rendering Overhead. Aber 25.000 Seiten bedeutet 25.000 Gelegenheiten, eine schlechte Entscheidung über deinen Component Tree zu treffen.
Unser größtes Performance-Problem war die Provider-Vergleichstabelle. Sie war eine schwere Client-Side React Komponente – viel State, viel Conditional 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 Site, wo der primäre Traffic Leute sind, die sich mitten in einer Kaufentscheidung befinden.Largest Contentful Paint of 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-gerenderte statische Tabelle mit einer dünnen React-Hydration-Schicht für die interaktiven Filter-Bits umgebaut. LCP ist auf 1,9 Sekunden gesunken. Das ist keine Magie – es ist einfach das langweilige Ding richtig machen.
Das Image-Problem
Jeder Provider hat ein Logo. 400 Logos, plus Screenshots, UI Previews, Feature-Icons. Wir haben den Fehler gemacht, die ersten zwei Monate auf Vercel's eingebaute Image-Optimierung zu hosten. Die Bandbreitenekosten waren leise entsetzlich. Alles zu Cloudflare R2 mit einer Custom Domain verschoben, Vercel-Rechnung von £180/Monat auf £40/Monat gesenkt. Wenn du irgendetwas Bild-lastig baust, schau dir Cloudflare R2 früh an – der kostenlose Egress ist wirklich nützlich in Skalierung.Cloudflare R2 early -- 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 einem Railway Cron Job, schreibt zu Supabase Postgres -- Python scraper on a Railway cron job, writes to Supabase Postgres
- Admin-Ebene -- Retool-Dashboard für manuelle Bearbeitungen, Korrektionen und Provider-Flaggen -- Retool dashboard for manual edits, corrections, and provider flags
- Next.js-App -- Pages Router (wir haben angefangen, bevor App Router stabil genug war, um ihm zu vertrauen), bereitgestellt auf Vercel -- Pages router (we started before App Router was stable enough to trust), deployed on Vercel
- ISR + On-Demand-Revalidierung -- die Top ~2.000 Seiten vorgebaut, 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
- Analytik -- Plausible für datenschutzfreundliche Verkehrsdaten, Ahrefs für Ranking-Tracking -- Plausible for privacy-friendly traffic data, Ahrefs for ranking tracking
- Verfügbarkeitsüberwachung -- BetterUptime beobachtet die fünf verkehrsreichsten 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
- Zu breit angefangen. 25.000 Seiten war immer das Ziel, aber ich hätte mit 500 hochwertigen Seiten starten und 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.
- Revalidation von Anfang an nicht richtig eingerichtet. Wir haben zwei Monate mit vollständigen Neubauten verschwendet, die ISR überflüssig gemacht hätte.We wasted two months on full rebuilds that ISR would have made unnecessary.
- Das Google Sheet behalten. Eine Single Source of Truth hätte vom ersten Tag 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.
- Zu lange Vercel Image Optimization genutzt. Zu R2 sechs Wochen später gewechselt, als wir hätten sollten.Moved to R2 six weeks later than we should have.
- Den Route Tree nicht früh genug aufgeteilt. Schnelle und langsame Routes in denselben getStaticPaths Call gemischt und dann gewundert, warum die 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 zu sein. Das ist der Teil, den Tutorials nicht vermitteln -- 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 jeden Backlink bewahrt. Redirected Provider (z.B. wenn ein Host einen anderen übernimmt) bekommen einen 301 über die Next.js redirects config in next.config.js. Wir überprüfen die Status-Flags monatlich.status flag 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.js redirects config in next.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.Astro is interesting for mostly-static content sites, and I've been playing with it on a smaller project. But Next.js gave us the flexibility to have both static and dynamic sections in the same codebase, which mattered. For a purely static directory with no interactive features, Astro might be faster to build and cheaper to run. Ask me again in a year.
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 Riesenerfolg. Es macht Geld -- Affiliate-Provisionen, ein paar direkte Advertising-Deals -- und rangiert relativ gut für vielleicht 600 der Begriffe, auf die ich ursprünglich abzielte. Das ist in Ordnung. Es war ein Lernprojekt, das zufällig auch Umsatz generiert, und das ist die beste Art.
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.
