Quelque part fin 2022, je me suis convaincu que construire un répertoire d'hébergement web serait simple. Agréger les données, générer les pages, classer, monétiser. Propre. J'avais déjà fait du SEO programmatique auparavant — un outil immobilier localisé pour un client au Royaume-Uni, un site de comparaison SaaS qui a culminé à 40k visites mensuelles — donc je pensais que HostList serait un projet de six semaines. Ça a pris près de sept mois. Et ça a presque cassé quelques trucs : mon rythme de sommeil, la confiance d'un de mes développeurs junior, et une facture Vercel de 180£ par mois que je n'avais pas budgétisée.
C'est le post-mortem. Le vrai, pas la version LinkedIn.
---
Le cahier des charges que je me suis écrit
HostList était censé être simple. Un répertoire de fournisseurs d'hébergement web — mutualisé, VPS, dédié, WordPress géré — avec des pages individuelles pour chaque fournisseur, des pages de comparaison, des pages de catégories, et des pages basées sur la localisation (par ex. « meilleur hébergement en Allemagne »). Faites le calcul : ~400 fournisseurs × plusieurs types de pages × 20+ combinaisons de filtres. Vous arrivez à 25 000 pages plus vite que vous ne le penseriez.
J'ai choisi Next.js presque sans y réfléchir. Nous l'utilisons chez Seahawk pour la plupart de nos gros projets basés sur React. L'écosystème est mature, getStaticProps et getStaticPaths ont du sens pour la génération statique lourde en SEO, et je trouve personnellement le routage basé sur les fichiers plus facile à comprendre que Remix ou Gatsby à cette échelle.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.
La première vraie décision concernait la couche de données. J'ai rapidement écarté un CMS headless — je ne voulais pas payer les tarifs de Contentful pour 25 000 entrées, et je ne faisais pas confiance à un CMS pour gérer proprement les écritures programmatiques en masse. Nous avons choisi une base de données Postgres sur Supabase, avec une couche API Next.js légère en façade. Cette partie a en fait bien fonctionné. C'est presque tout le reste qui s'est compliqué.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.
---
Génération statique à l'échelle : ce que personne ne vous prévient
Voilà la réalité avec getStaticPaths et 25 000 routes. Ça marche. Techniquement. Mais vos temps de compilation vous feront remettre en question vos choix de vie.getStaticPathswith 25,000 routes. It works. Technically. But your build times will make you question your life choices.
Notre premier build complet a pris 4 heures et 47 minutes. Sur Vercel. Ce qui, si vous ne faites pas attention à vos limites de forfait, est le genre de chose qui déclenche une notification de facturation à 2h du matin. J'ai fixé cette alerte Slack depuis mon téléphone et j'ai sérieusement envisagé d'utiliser simplement WordPress.
Le piège de `fallback: 'blocking'`
Mon instinct initial était de pré-rendre tout. Chaque page, chaque combinaison. Mauvaise idée — et pas pour la raison que la plupart des tutoriels vous avertissent (c'est généralement juste « ça prend du temps »). Le vrai problème, c'est l'invalidation du cache. Quand un fournisseur d'hébergement met à jour ses tarifs (et ils le font, constamment), vous devez reconstruire les pages affectées. Si tout est pré-rendu statiquement sans ISR, vous déclenchez des reconstructions complètes pour des changements de données qui ne concernent peut-être que 30 pages sur 25 000.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.
J'ai basculé vers la Régénération Statique Incrémentale avec un revalidate de 86400 secondes (24 heures) pour la plupart des pages, et 3600 secondes pour les pages de fournisseurs lourdes en tarifs. C'était l'amélioration de qualité de vie unique la plus importante du projet entier. Les temps de compilation sont passés sous les 40 minutes parce que nous ne pré-rendions que le top ~2 000 pages par priorité de trafic et laissions le reste générer à la demande avec fallback: 'blocking'.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'.
Scinder l'arborescence des routes
Une chose que je ferais différemment, et que je dis maintenant à chaque dev de Seahawk qui touche à un gros projet programmatique : divisez votre arborescence de routes en amont. N'ayez pas une fonction getStaticPaths monolithique qui essaie de retourner 25 000 slugs. Nous avons divisé la nôtre en :getStaticPathsfunction trying to return 25,000 slugs. We broke ours into:
/providers/[slug] — pages de fournisseurs individuels (~400)— individual provider pages (~400)/compare/[slugA]-vs-[slugB] — pages de comparaison directe (~8 000)— head-to-head comparison pages (~8,000)/category/[type] — pages d'accueil de catégories (~40)— category landing pages (~40)/location/[country]/[type] — combinaisons géo × catégories (~16 000+)— geo × category combinations (~16,000+)/best/[use-case] — pages de listes curées (~600)— curated list pages (~600)
Chaque groupe de routes a son propre cadence de revalidation, sa propre logique de récupération de données, et de façon critique, sa propre priorité de build. Les pages de localisation sont presque entièrement à la demande. Les pages de fournisseurs sont toujours pré-rendues. Séparation propre.
---
Le Fouillis du Pipeline de Données (Et Comment Nous L'Avons Réglé)
Au début de 2023, j'ai commis l'erreur de construire le côté collecte de données de HostList trop librement. Nous avions un script de scraping (écrit en Python, utilisant BeautifulSoup et un pool de proxies rotatifs de Webshare), un Google Sheet manuel pour les corrections, et une table Supabase. Trois sources de vérité. Aucune d'elles ne communiquant correctement entre elles.
Un junior dev — bon gamin, tout juste sorti d'un bootcamp — a passé trois semaines à maintenir un script de synchronisation entre la Sheet et Supabase qui cassait à chaque fois qu'un nom de colonne changeait. J'aurais dû tuer la Sheet la semaine une et construire une véritable interface admin interne. On l'a fait finalement, avec des routes API Next.js et un dashboard Retool accrochés sur le côté, mais on a brûlé probablement 60 heures d'ingénierie pour y arriver.
Le correctif : une seule source de vérité, toujours. La base de données est canonique. Tout écrit dans la base de données. L'interface admin lit et écrit dans la base de données. Le scraper écrit dans la base de données. Ça paraît évident. Ça l'est toujours, avec le recul.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.
Garder les données à jour à grande échelle
Pour un annuaire de cette taille, la fraîcheur des données est une préoccupation SEO autant qu'une question d'UX. Google remarque quand les tableaux de prix affichent £2.99/mois pour un forfait qui coûte £5.99 depuis huit mois. On a mis en place :
- Un job de scrape hebdomadaire fonctionnant sur une cron Railway (bon marché, fiable, ne nécessite pas de serveur dédié)
- Un webhook de base de données Supabase qui se déclenche quand une colonne price_updated_at change, frappant un endpoint de revalidation Next.js
price_updated_atcolumn changes, hitting a Next.js revalidation endpoint - Des drapeaux de remplacement manuel dans Retool pour les ~30 fournisseurs dont les sites bloquent activement les scrapers
Cet endpoint de revalidation — /api/revalidate?secret=TOKEN&path=/providers/siteground — est une fonctionnalité stock Next.js, mais le connecter à un webhook de base de données a demandé un peu de plomberie. Ça valait chaque minute./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.
---
Architecture SEO : ce qui a réellement bougé l'aiguille
J'ai créé assez de sites de contenu pour savoir que disposer de 25 000 pages n'est pas la même chose que d'avoir 25 000 pages qui se classent. Les pages de comparaison ont été le piège. Nous avons généré toutes les combinaisons A-vs-B possibles pour nos ~400 fournisseurs, ce qui nous a donné environ 79 800 appairements théoriques. Nous en avons construit ~8 000. Et la plupart d'entre eux étaient, honnêtement, minces.
Confession honnête : j'ai été gourmand. La logique SEO était solide — « SiteGround vs Bluehost » génère un vrai volume de recherche, la longue traîne des requêtes de comparaison est énorme — mais nous n'avons pas créé assez de contenu unique par page pour justifier l'existence de chacune d'elles. Google a commencé à explorer la section de comparaison et a clairement décidé que ce n'était pas worth son temps. Les propres conseils de Google sur le contenu fin sont explicites sur ce point, et j'aurais dû être plus franc avec moi-même plus tôt.Google's own guidance on thin contentis blunt about this, and I should have been blunter with myself earlier.
Ce que nous avons fait pour récupérer
Nous avons élagué. Réduit les pages de comparaison d'~8 000 à ~1 200 — uniquement les paires avec un volume de recherche démontrable (vérifié dans Ahrefs, minimum 50 recherches mensuelles au niveau mondial). Puis nous avons enrichi les pages restantes avec :
- Des sections dynamiques « c'est mieux pour » extraites des données structurées des fournisseurs
- Des données réelles de disponibilité (nous avons intégré une API de disponibilité tierce)
- Des résumés d'avis utilisateurs alimentés par les données de Trustpilot le cas échéant
Le résultat : 1 200 pages qui étaient réellement utiles au lieu de 8 000 pages qui ne l'étaient pas. Le trafic organique vers la section de comparaison a augmenté de 340% au cours des trois mois suivants. Contre-intuitif jusqu'au moment où ça ne l'est plus.
Liaison interne à cette échelle
Avec 25 000 pages, la liaison interne ne peut pas être manuelle. Nous avons construit un composant de pages connexes qui interroge Supabase au moment de la compilation (dans getStaticProps) et retourne les cinq pages adjacentes les plus pertinentes en fonction du chevauchement de catégories et de localisation. Aucune intervention éditoriale nécessaire. Ce n'est pas parfait — occasionnellement une page d'hébergement VPS renvoie vers quelque chose d'un peu décalé — mais c'est correct à 90%, et cela signifiait que chaque page avait des liens internes contextuellement pertinents dès le premier jour.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 : La partie qui vous humilie
On croirait que la génération statique rendrait la performance facile. Et sur le plan conceptuel, c'est vrai — HTML pré-rendu, mis en cache en edge sur le CDN de Vercel, pas de surcharge de rendu serveur. Mais 25 000 pages, c'est 25 000 occasions d'avoir pris une mauvaise décision concernant votre arborescence de composants.
Notre plus gros problème de performance était le tableau comparatif des fournisseurs. C'était un lourd composant React côté client — beaucoup d'état, beaucoup de rendu conditionnel, utilisé à la fois sur les pages fournisseurs et les pages de comparaison. Sur mobile, il provoquait un Largest Contentful Paint d'environ 4,8 secondes. Mauvais. Vraiment mauvais pour un site où le trafic principal vient de gens en pleine décision d'achat.Largest Contentful Paintof around 4.8 seconds. Bad. Really bad for a site where the primary traffic is people mid-decision on a purchase.
Nous l'avons reconstruit en tant que tableau statique rendu côté serveur avec une fine couche d'hydratation React pour les bits de filtre interactifs. Le LCP est tombé à 1,9 secondes. Ce n'est pas de la magie — c'est juste faire la chose ennuyeuse correctement.
Le problème des images
Chaque fournisseur a un logo. 400 logos, plus des captures d'écran, des aperçus d'interface, des icônes de fonctionnalités. Nous avons commis l'erreur d'héberger ceux-ci sur l'optimisation d'image intégrée de Vercel pendant les deux premiers mois. Les coûts de bande passante étaient silencieusement horrifiants. Nous avons tout déplacé vers Cloudflare R2 avec un domaine personnalisé, ce qui a réduit notre facture Vercel de 180 £/mois à 40 £/mois. Si vous construisez quelque chose de lourd en images, regardez Cloudflare R2 tôt — la sortie gratuite est véritablement utile à l'échelle.Cloudflare R2early — the free egress is genuinely useful at scale.
---
À quoi ressemble vraiment le pipeline de build maintenant
Pour tous ceux qui veulent le tableau concret :
- Collecte de données — scraper Python sur une tâche cron Railway, écrit dans Supabase Postgres— Python scraper on a Railway cron job, writes to Supabase Postgres
- Couche admin — tableau de bord Retool pour les modifications manuelles, les corrections et les marquages de fournisseurs— Retool dashboard for manual edits, corrections, and provider flags
- Application Next.js — Pages router (nous avons commencé avant que App Router soit assez stable pour être fiable), déployée sur Vercel— Pages router (we started before App Router was stable enough to trust), deployed on Vercel
- ISR + revalidation à la demande — environ 2 000 pages du haut pré-construites, le reste à la demande, toutes avec revalidation de 24h— top ~2,000 pages pre-built, rest on-demand, all with 24h revalidation
- Images — Cloudflare R2, servies via un sous-domaine personnalisé avec CDN Cloudflare en frontal— Cloudflare R2, served via a custom subdomain with Cloudflare CDN in front
- Analytics — Plausible pour les données de trafic respectueuses de la vie privée, Ahrefs pour le suivi des classements— Plausible for privacy-friendly traffic data, Ahrefs for ranking tracking
- Monitoring d'uptime — BetterUptime surveille les cinq types de pages les plus chargés en trafic— BetterUptime watching the five most traffic-heavy page types
Ce n'est pas glamour. C'est aussi largement ennuyeux à maintenir, ce qui est exactement ce que vous voulez d'une infrastructure que vous allez laisser tourner pendant trois ans.
---
Erreurs Honnêtes, Numérotées
- J'ai commencé trop large. 25 000 pages était toujours l'objectif, mais j'aurais dû lancer avec 500 pages de haute qualité et développer progressivement. À la place, j'ai lancé avec tout et j'ai eu un problème de budget de crawl Google pendant les quatre premiers mois.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.
- Je n'ai pas configuré la revalidation correctement dès le premier jour. On a perdu deux mois sur des rebuilds complets qu'ISR aurait rendus inutiles.We wasted two months on full rebuilds that ISR would have made unnecessary.
- J'ai gardé la Google Sheet. Une source unique de vérité aurait dû être non négociable dès la première semaine.Single source of truth should have been non-negotiable from week one.
- J'ai sous-estimé la qualité des pages de comparaison. Le volume n'est pas une stratégie.Volume is not a strategy.
- J'ai utilisé l'optimisation d'images de Vercel trop longtemps. On a basculé sur R2 six semaines plus tard qu'on aurait dû.Moved to R2 six weeks later than we should have.
- Je n'ai pas scindé l'arbre des routes assez tôt. J'ai mélangé les routes rapides et lentes dans le même appel getStaticPaths et j'ai été surpris que les builds soient lents.Mixed fast and slow routes in the same
getStaticPathscall and then wondered why builds were slow.
Chacune de ces décisions semblait raisonnable à l'époque. C'est ce que les tutoriels ne capturent pas — les mauvaises décisions architecturales ont généralement des justifications qui sonnent bien quand on les prend.
---
FAQ
Combien de temps a pris le build initial pour se mettre en ligne ?
Sept mois entre le premier commit et une version que j'étais à l'aise d'appeler v1. La première version publique brute était en ligne après environ quatre mois, mais elle avait des problèmes sérieux de contenu insuffisant et la section de comparaison était surtout inutile. Je dirais quatre mois pour « techniquement en ligne » et trois autres pour « réellement bon ».
Utiliseriez-vous l'App Router si vous commenciez aujourd'hui ?
Probablement oui, pour les nouveaux projets lancés fin 2023 et après. Les composants serveur de l'App Router seraient bien adaptés à ce genre de génération de pages gourmandes en données. Mais migrer une appli Pages Router existante de 25 000 pages n'est pas un projet que je vais entreprendre de sitôt. Le Pages Router fonctionne toujours, et « fonctionner » c'est sous-estimé.
Comment gérez-vous les fournisseurs qui cessent leurs activités ou changent considérablement leur offre ?
Nous avons un indicateur de statut dans la base de données — actif, obsolète, redirigé. Les fournisseurs obsolètes obtiennent une page d'archive épurée plutôt qu'une suppression complète, ce qui préserve les backlinks. Les fournisseurs redirigés (par exemple quand un hébergeur en acquiert un autre) obtiennent une redirection 301 gérée via la configuration des redirects de Next.js dans next.config.js. Nous examinons les indicateurs de statut mensuellement.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.
Qu'utiliseriez-vous à la place de Next.js si vous refaisiez cela ?
Je ne sais vraiment pas. Astro est intéressant pour les sites de contenu surtout statique, et j'ai expérimenté avec sur un projet plus petit. Mais Next.js nous a donné la flexibilité d'avoir à la fois des sections statiques et dynamiques dans la même base de code, ce qui importait. Pour un répertoire purement statique sans fonctionnalités interactives, Astro pourrait être plus rapide à construire et moins cher à exploiter. Posez-moi la question à nouveau dans un an.
Comment empêchez-vous les scrapers de copier tout le répertoire ?
Honnêtement ? Vous ne pouvez pas, complètement. Nous limitons la fréquence des requêtes sur les routes API, utilisons la gestion des bots de Cloudflare sur le frontend, et faisons tourner une partie des données structurées pour que les copies scrapées deviennent rapidement obsolètes. Mais si quelqu'un veut cloner un répertoire accessible publiquement, il trouvera un moyen. Le moat c'est la fraîcheur des données et la qualité de l'UX, pas l'obfuscation technique.
---
Pensée Finale
HostList n'est pas un succès fulgurant. Ça génère de l'argent — commissions d'affiliation, quelques accords publicitaires directs — et se classe raisonnablement bien pour peut-être 600 des termes que je visais initialement. C'est acceptable. C'était un projet d'apprentissage qui se trouve aussi générer du revenu, ce qui est le mieux qu'on puisse avoir.
Si vous envisagez de construire un grand site SEO programmatique sur Next.js, mon conseil honnête est celui-ci : faites-le. C'est véritablement une bonne stack pour le travail. Mais construisez moins que ce que vous pensez avoir besoin, construisez-le mieux que ce que vous pensez avoir le temps de faire, et réglez votre architecture de données avant d'écrire un seul modèle de page.
La technologie, c'est la partie facile. Ça l'est toujours.
