Vers la fin de 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, les classer, monétiser. Propre. J'avais fait du SEO programmatique avant, un outil immobilier localisé pour un client au Royaume-Uni, un site de comparaison SaaS qui avait atteint 40 000 visites mensuelles, donc j'imaginais que HostList serait un projet de six semaines. Ça a pris près de sept mois. Et ça a presque cassé quelques choses : mon horaire de sommeil, la confiance d'une de mes développeuses junior, et une facture Vercel de 180 £/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 managé, avec des pages individuelles pour chaque fournisseur, des pages de comparaison, des pages de catégorie, 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 réfléchir. Nous l'utilisons chez Seahawk pour la plupart de nos gros projets React. L'écosystème est mature, getStaticProps et getStaticPaths ont du sens pour la génération statique lourde en SEO, et personnellement je trouve le routage basé sur les fichiers plus facile à raisonner que Remix ou Gatsby à cette échelle.
La première vraie décision était la couche de données. J'ai rapidement écarté un CMS headless, je ne voulais pas payer les tarifs Contentful pour 25 000 entrées, et je ne faisais pas confiance à un CMS pour gérer proprement les écritures programmatiques en masse. Nous nous sommes arrêtés sur une base de données Postgres sur Supabase, avec une couche API Next.js légère en face. Cette partie a en fait bien fonctionné. C'est à peu près tout le reste qui s'est compliqué.
---
Génération statique à l'échelle : ce que personne ne vous prévient
Voilà le truc avec getStaticPaths et 25 000 routes. Ça marche. Techniquement. Mais vos temps de build vont vous faire remettre en question vos choix de vie.
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 mettent en garde (qui est généralement juste « ça prend du temps »). Le vrai problème 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 affectent peut-être 30 pages sur 25 000.
J'ai basculé sur Incremental Static Regeneration avec une revalidate de 86400 secondes (24 heures) pour la plupart des pages, et 3600 secondes pour les pages de fournisseurs lourdes en tarifs. C'a été la meilleure amélioration qualité de vie du projet entier. Les temps de build sont tombés à moins de 40 minutes parce que nous ne pré-rendions que les ~2 000 meilleures pages par priorité de trafic et laissions le reste générer à la demande avec fallback: 'blocking'.
Scinder l'arborescence des routes
Une chose que je ferais différemment, et que je dis à tous les devs chez Seahawk qui touchent à un gros projet programmatique maintenant : divisez votre arbre de routes tôt. N'ayez pas une fonction monolithique getStaticPaths essayant de retourner 25 000 slugs. Nous avons divisé la nôtre en :
- /providers/[slug], pages de fournisseur individuelles (~400)
- /compare/[slugA]-vs-[slugB], pages de comparaison tête-à-tête (~8 000)
- /category/[type], pages d'accueil des catégories (~40)
- /location/[country]/[type], combinaisons géo × catégorie (~16 000+)
- /best/[use-case], pages de listes curatoriales (~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, un bon gars, tout juste sorti d'un bootcamp, a passé trois semaines à maintenir un script de synchronisation entre la Sheet et Supabase qui se cassait à chaque fois qu'un nom de colonne changeait. J'aurais dû tuer la Sheet à la semaine une et construire une véritable UI admin interne. On l'a finalement fait, avec des routes API Next.js et un dashboard Retool attaché à côté, mais on a brûlé probablement 60 heures d'engineering pour y arriver.
La solution : 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. Cela semble évident. Ça l'est toujours, avec le recul.
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, en frappant un endpoint de revalidation Next.js
- Des drapeaux de remplacement manuel dans Retool pour les ~30 fournisseurs dont les sites bloquent activement les scrapers
Ce endpoint de revalidation, /api/revalidate?secret=TOKEN&path=/providers/siteground, c'est une feature stock de Next.js, mais la brancher à un webhook de base de données a demandé un peu de tuyauterie. Ça en valait chaque 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.
Aveu honnête : j'ai eu les yeux plus gros que le ventre. La logique SEO était solide, « SiteGround vs Bluehost » génère un vrai volume de recherche, la long-tail des requêtes de comparaison est énorme, mais on n'a pas construit assez de contenu unique par page pour justifier l'existence de chacune d'elles. Google a commencé à crawler la section comparaison et a clairement décidé que ça ne valait pas son temps. Les propres recommandations de Google sur le contenu mince sont sans détour à ce sujet, et j'aurais dû être plus direct avec moi-même plus tôt.
Ce que nous avons fait pour récupérer
On a fait du tri. Réduit les pages de comparaison de ~8 000 à ~1 200, seulement les paires avec un volume de recherche démontrable (vérifié dans Ahrefs, minimum 50 recherches mensuelles globalement). Ensuite on a 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, les liens internes ne peuvent pas être manuels. On a construit un composant de pages associées qui interroge Supabase au moment du build (dans getStaticProps) et retourne les cinq pages adjacentes les plus pertinentes en fonction du chevauchement de catégorie et de localisation. Aucune intervention éditoriale nécessaire. Ce n'est pas parfait, occasionnellement une page d'hébergement VPS fait lien vers quelque chose d'un peu de travers, mais c'est correct à 90 %, et ça a signifié que chaque page avait des liens internes contextuellement pertinents dès le premier jour.
---
Performance : La partie qui vous humilie
On pourrait penser que la génération statique rendrait la performance facile. Et à un niveau conceptuel, c'est le cas, du HTML pré-rendu, en cache edge sur le CDN de Vercel, zéro surcharge de rendu côté serveur. Mais 25 000 pages signifie 25 000 occasions d'avoir fait une mauvaise décision sur votre arbre de composants.
Notre plus gros problème de performance était la table de comparaison des providers. C'était un composant React lourd côté client, beaucoup d'état, beaucoup de rendu conditionnel, utilisé à la fois sur les pages de provider et les pages de comparaison. Sur mobile, ça causait un Largest Contentful Paint d'environ 4,8 secondes. Mauvais. Vraiment mauvais pour un site où le trafic principal c'est des gens en pleine décision d'achat.
On l'a reconstruite comme une table statique rendue côté serveur avec une fine couche d'hydratation React pour les bits de filtre interactifs. L'LCP a chuté à 1,9 secondes. Ce n'est pas de la magie, c'est juste faire le truc chiant correctement.
Le problème des images
Chaque provider a un logo. 400 logos, plus des screenshots, des previews UI, des icônes de features. On a fait l'erreur d'héberger ceux-ci sur l'optimisation d'images intégrée de Vercel les deux premiers mois. Les coûts de bande passante étaient tranquillement horrifiants. On a tout déplacé vers Cloudflare R2 avec un domaine personnalisé, on 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 réellement utile à l'échelle.
---
À quoi ressemble vraiment le pipeline de build maintenant
Pour tous ceux qui veulent le tableau concret :
- Collecte de données, scraper Python sur un cron job Railway, écrit dans Supabase Postgres
- Couche admin, tableau de bord Retool pour les modifications manuelles, corrections et marquage des fournisseurs
- App Next.js, Pages router (nous avons commencé avant que l'App Router soit suffisamment stable pour qu'on lui fasse confiance), déployée sur Vercel
- ISR + revalidation à la demande, les ~2 000 pages principales pré-construites, le reste à la demande, tout avec une revalidation de 24h
- Images, Cloudflare R2, servies via un sous-domaine personnalisé avec CDN Cloudflare en front
- Analytics, Plausible pour les données de trafic respectueuses de la vie privée, Ahrefs pour le suivi des classements
- Surveillance de disponibilité, BetterUptime surveille les cinq types de pages les plus lourds en trafic
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 m'étendre 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.
- Je n'ai pas mis en place la revalidation correctement dès le premier jour. Nous avons perdu deux mois sur des reconstructions complètes qu'ISR aurait rendues inutiles.
- J'ai gardé le Google Sheet. Une source de vérité unique aurait dû être non-négociable dès la première semaine.
- J'ai sous-estimé la qualité des pages de comparaison. Le volume n'est pas une stratégie.
- Utilisation trop longue de l'optimisation d'images Vercel. Migration vers R2 six semaines plus tard que prévu.
- On n'a pas divisé l'arborescence des routes assez tôt. Routes rapides et lentes mélangées dans le même appel getStaticPaths et on se demandait ensuite pourquoi les builds étaient lents.
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 de bonnes justifications au moment où vous les prenez.
---
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 flag de statut dans la base de données : active, deprecated, redirected. Les fournisseurs deprecated reçoivent une page d'archive allégée plutôt qu'une suppression complète, ce qui préserve les backlinks. Les fournisseurs redirected (par exemple quand un hébergeur en acquiert un autre) reçoivent un 301 géré via la config redirects de Next.js dans next.config.js. Nous examinons les flags de statut tous les mois.
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 fou. Elle génère des revenus, des commissions d'affiliation, quelques contrats publicitaires directs, et elle se classe correctement pour environ 600 des termes que je ciblais à l'origine. C'est correct. C'était un projet d'apprentissage qui génère aussi des revenus, ce qui est le mieux qu'on puisse demander.
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.
