A finales de 2022 me convencí a mí mismo de que construir un directorio de hosting web sería sencillo. Agregar datos, generar páginas, posicionar, monetizar. Limpio. Había hecho jugadas de SEO programático antes — una herramienta de bienes raíces localizada para un cliente en Reino Unido, un sitio de comparación SaaS que alcanzó 40k visitas mensuales — así que pensé que HostList sería un proyecto de seis semanas. Tomó cerca de siete meses. Y casi rompe algunas cosas: mi horario de sueño, la confianza de uno de mis desarrolladores junior, y una factura de Vercel de £180/mes que no había presupuestado.
Este es el post-mortem. El real, no la versión de LinkedIn.
---
El brief que me escribí a mí mismo
HostList se suponía que sería simple. Un directorio de proveedores de hosting web — compartido, VPS, dedicado, WordPress gestionado — con páginas individuales para cada proveedor, páginas de comparación, páginas de categoría, y páginas basadas en ubicación (p. ej. "mejor hosting en Alemania"). Haz las matemáticas: ~400 proveedores × varios tipos de página × 20+ combinaciones de filtros. Llegas a 25,000 páginas más rápido de lo que piensas.
Elegí Next.js casi sin pensarlo. Lo usamos en Seahawk para la mayoría de nuestros proyectos más grandes basados en React. El ecosistema es maduro, getStaticProps y getStaticPaths tienen sentido para la generación estática con mucho SEO, y personalmente encuentro el enrutamiento basado en archivos más fácil de razonar que Remix o Gatsby en esta escala.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 primera decisión real fue la capa de datos. Descarté un CMS sin cabecera bastante rápido — no quería pagar tarifas de Contentful por 25,000 entradas, y no confiaba en que un CMS manejara escrituras programáticas en masa de forma limpia. Terminamos con una base de datos Postgres en Supabase, con una capa API ligera de Next.js sentada frente a ella. Esa parte en realidad funcionó bien. Es casi todo lo demás lo que se complicó.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.
---
Generación Estática a Escala: Lo Que Nadie Te Advierte
La cuestión con getStaticPaths y 25,000 rutas es esta: funciona. Técnicamente. Pero tus tiempos de compilación te harán cuestionarte tus decisiones de vida.getStaticPathswith 25,000 routes. It works. Technically. But your build times will make you question your life choices.
Nuestra primera compilación completa tomó 4 horas y 47 minutos. En Vercel. Lo que, si no tienes cuidado con los límites de tu plan, es el tipo de cosa que causa una notificación de facturación a las 2am. Miré esa alerta de Slack desde mi teléfono y genuinamente consideré simplemente usar WordPress.
La Trampa de `fallback: 'blocking'`
Mi instinto inicial fue pre-renderizar todo. Cada página, cada combinación. Mala idea — y no por la razón que la mayoría de tutoriales te advierten (que usualmente es solo "toma un tiempo"). El problema real es la invalidación de caché. Cuando un proveedor de hosting actualiza sus precios (y lo hacen, constantemente), necesitas reconstruir las páginas afectadas. Si todo está pre-renderizado estáticamente sin ISR, estás desencadenando reconstrucciones completas para cambios de datos que afectan quizá 30 páginas de 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.
Cambié a Regeneración Estática Incremental con un revalidate de 86400 segundos (24 horas) para la mayoría de páginas, y 3600 segundos para páginas de proveedores pesadas en precios. Esta fue la única mejora más grande en la calidad de vida de todo el proyecto. Los tiempos de compilación cayeron a menos de 40 minutos porque solo estábamos pre-renderizando las ~2,000 páginas principales por prioridad de tráfico y dejando que el resto se generara bajo demanda con 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'.
Dividiendo el Árbol de Rutas
Una cosa que haría diferente, y que les digo a todos los devs en Seahawk que tocan un proyecto programático grande ahora: divide tu árbol de rutas temprano. No tengas una función getStaticPaths monolítica tratando de retornar 25,000 slugs. Nosotros dividimos la nuestra en:getStaticPathsfunction trying to return 25,000 slugs. We broke ours into:
/providers/[slug] — páginas individuales de proveedores (~400)— individual provider pages (~400)/compare/[slugA]-vs-[slugB] — páginas de comparación directa (~8,000)— head-to-head comparison pages (~8,000)/category/[type] — páginas de inicio de categoría (~40)— category landing pages (~40)/location/[country]/[type] — combinaciones geográficas × categoría (~16,000+)— geo × category combinations (~16,000+)/best/[use-case] — páginas de listas seleccionadas (~600)— curated list pages (~600)
Cada grupo de rutas tiene su propio cadence de revalidación, su propia lógica de obtención de datos, y críticamente, su propia prioridad de compilación. Las páginas de ubicación son casi completamente bajo demanda. Las páginas de proveedores siempre se pre-renderizan. Separación clara.
---
El Desastre de la Canalización de Datos (Y Cómo Lo Arreglamos)
A principios de 2023 cometí el error de construir el lado de recopilación de datos de HostList demasiado suelto. Teníamos un script de scraping (escrito en Python, usando BeautifulSoup y un pool de proxies rotativo de Webshare), una Google Sheet manual para correcciones, y una tabla de Supabase. Tres fuentes de verdad. Ninguna de ellas comunicándose adecuadamente entre sí.
Un desarrollador junior —buen chico, recién salido de un bootcamp— pasó tres semanas manteniendo un script de sincronización entre la Sheet y Supabase que se rompía cada vez que cambiaba un nombre de columna. Debería haber eliminado la Sheet en la primera semana y construido una UI de admin adecuada. Eventualmente lo hicimos, usando Next.js API routes y un dashboard de Retool acoplado al lado, pero quemamos probablemente 60 horas de ingeniería para llegar ahí.
La solución: una única fuente de verdad, siempre. La base de datos es canónica. Todo se escribe en la base de datos. La UI de admin lee y escribe en la base de datos. El scraper escribe en la base de datos. Suena obvio. Siempre lo hace, en retrospectiva.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.
Mantener Datos Frescos a Escala
Para un directorio de este tamaño, la frescura de datos es una preocupación de SEO tanto como de UX. Google se da cuenta cuando las tablas de precios muestran £2.99/mes para un plan que ha estado en £5.99 durante ocho meses. Configuramos:
- Un trabajo de scrape semanal ejecutándose en un cron de Railway (barato, confiable, no requiere un servidor dedicado)
- Un webhook de base de datos Supabase que se dispara cuando cambia una columna price_updated_at, golpeando un endpoint de revalidación Next.js
price_updated_atcolumn changes, hitting a Next.js revalidation endpoint - Banderas de anulación manual en Retool para los ~30 proveedores cuyos sitios bloquean activamente scrapers
Ese endpoint de revalidación —/api/revalidate?secret=TOKEN&path=/providers/siteground— es una característica estándar de Next.js, pero conectarlo a un webhook de base de datos requirió un poco de fontanería. Valió cada minuto./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.
---
Arquitectura de SEO: Lo Que Realmente Movió la Aguja
He construido suficientes sitios de contenido para saber que tener 25,000 páginas no es lo mismo que tener 25,000 páginas que rankeen. Las páginas de comparación fueron la trampa. Generamos cada combinación A-vs-B posible para nuestros ~400 proveedores, lo que nos dio aproximadamente 79,800 emparejamientos teóricos. Construimos ~8,000 de ellos. Y la mayoría de ellos eran, francamente, delgados.
Confesión honesta: me volví codicioso. La lógica de SEO era sólida — "SiteGround vs Bluehost" obtiene volumen de búsqueda real, la cola larga de consultas de comparación es enorme — pero no construimos suficiente contenido único por página para justificar la existencia de cada una. Google comenzó a rastrear la sección de comparación y claramente decidió que no valía su tiempo. La propia guía de Google sobre contenido delgado es contundente al respecto, y debería haber sido más contundente conmigo mismo antes.Google's own guidance on thin contentis blunt about this, and I should have been blunter with myself earlier.
Qué Hicimos para Recuperarnos
Podamos. Redujimos las páginas de comparación de ~8,000 a ~1,200 — solo pares con volumen de búsqueda demostrable (verificado en Ahrefs, mínimo 50 búsquedas mensuales globales). Luego enriquecimos las páginas restantes con:
- Secciones dinámicas "para quién es mejor" extraídas de datos estructurados del proveedor
- Datos de uptime reales (nos integramos con una API de uptime de terceros)
- Resúmenes de reseñas de usuarios sembrados desde datos de Trustpilot cuando están disponibles
El resultado fue 1,200 páginas que realmente eran útiles en lugar de 8,000 páginas que no lo eran. El tráfico orgánico a la sección de comparación aumentó 340% durante los siguientes tres meses. Contraintuitivo hasta que no lo es.
Enlazado Interno a Esta Escala
Con 25,000 páginas, el enlazado interno no puede ser manual. Construimos un componente de páginas relacionadas que consulta Supabase en tiempo de compilación (en getStaticProps) y devuelve las cinco páginas adyacentes más relevantes basadas en solapamiento de categoría y ubicación. Sin intervención editorial necesaria. No es perfecto — ocasionalmente una página de hosting VPS enlaza a algo un poco sesgado — pero tiene razón el 90% del tiempo, y significó que cada página tenía enlaces internos contextualmente relevantes desde el primer día.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.
---
Rendimiento: La parte que te humilla
Dirías que la generación estática haría el rendimiento fácil. Y a nivel conceptual, así es — HTML pre-renderizado, cacheado en el edge en la CDN de Vercel, sin overhead de server-rendering. Pero 25,000 páginas significa 25,000 oportunidades de haber tomado una mala decisión sobre tu árbol de componentes.
Nuestro mayor problema de rendimiento fue la tabla de comparación de proveedores. Era un componente React pesado del lado del cliente — mucho estado, mucho renderizado condicional, usado tanto en páginas de proveedor como en páginas de comparación. En móvil, causaba un Largest Contentful Paint de alrededor de 4.8 segundos. Malo. Realmente malo para un sitio donde el tráfico principal son personas en medio de una decisión de compra.Largest Contentful Paintof around 4.8 seconds. Bad. Really bad for a site where the primary traffic is people mid-decision on a purchase.
La reconstruimos como una tabla estática renderizada en servidor con una capa de hidratación React delgada para las partes interactivas del filtro. El LCP bajó a 1.9 segundos. Eso no es magia — es solo hacer lo aburrido correctamente.
El problema de las imágenes
Cada proveedor tiene un logo. 400 logos, más capturas de pantalla, vistas previas de UI, iconos de características. Cometimos el error de alojar estos en la optimización de imágenes incorporada de Vercel durante los primeros dos meses. Los costos de ancho de banda fueron silenciosamente horribles. Movimos todo a Cloudflare R2 con un dominio personalizado, redujimos nuestra factura de Vercel de £180/mes a £40/mes. Si estás construyendo cualquier cosa con mucha densidad de imágenes, mira Cloudflare R2 temprano — el egreso gratuito es genuinamente útil a escala.Cloudflare R2early — the free egress is genuinely useful at scale.
---
Cómo se ve realmente el pipeline de construcción ahora
Para cualquiera que quiera el panorama concreto:
- Recopilación de datos — scraper Python en un cron job de Railway, escribe en Supabase Postgres— Python scraper on a Railway cron job, writes to Supabase Postgres
- Capa de administración — dashboard de Retool para ediciones manuales, correcciones y flags de proveedores— Retool dashboard for manual edits, corrections, and provider flags
- Aplicación Next.js — Pages router (empezamos antes de que App Router fuera lo suficientemente estable), desplegada en Vercel— Pages router (we started before App Router was stable enough to trust), deployed on Vercel
- ISR + revalidación bajo demanda — las ~2.000 páginas principales pre-construidas, el resto bajo demanda, todas con revalidación cada 24h— top ~2,000 pages pre-built, rest on-demand, all with 24h revalidation
- Imágenes — Cloudflare R2, servidas a través de un subdominio personalizado con Cloudflare CDN al frente— Cloudflare R2, served via a custom subdomain with Cloudflare CDN in front
- Analítica — Plausible para datos de tráfico respetuosos con la privacidad, Ahrefs para seguimiento de rankings— Plausible for privacy-friendly traffic data, Ahrefs for ranking tracking
- Monitoreo de disponibilidad — BetterUptime vigilando los cinco tipos de página con más tráfico— BetterUptime watching the five most traffic-heavy page types
No es glamoroso. Tampoco es particularmente emocionante mantenerlo, que es exactamente lo que quieres de una infraestructura que vas a dejar corriendo durante tres años.
---
Errores Honestos, Numerados
- Comencé demasiado amplio. 25,000 páginas era siempre el objetivo, pero debería haber lanzado con 500 páginas de alta calidad y expandido. En su lugar lancé con todo y tuve un problema de presupuesto de rastreo de Google durante los primeros cuatro meses.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.
- No configuré la revalidación correctamente desde el primer día. Perdimos dos meses en reconstrucciones completas que ISR hubiera hecho innecesarias.We wasted two months on full rebuilds that ISR would have made unnecessary.
- Mantuve la Hoja de Cálculo de Google. La fuente única de verdad debería haber sido innegociable desde la primera semana.Single source of truth should have been non-negotiable from week one.
- Subestimé la calidad de las páginas de comparación. El volumen no es una estrategia.Volume is not a strategy.
- Usé la optimización de imágenes de Vercel durante demasiado tiempo. Pasamos a R2 seis semanas después de lo que debería haber sido.Moved to R2 six weeks later than we should have.
- No dividí el árbol de rutas lo suficientemente temprano. Mezclé rutas rápidas y lentas en la misma llamada getStaticPaths y luego me pregunté por qué las compilaciones eran lentas.Mixed fast and slow routes in the same
getStaticPathscall and then wondered why builds were slow.
Cada una de estas es una decisión que parecía razonable en su momento. Esa es la parte que los tutoriales no capturan — las decisiones arquitectónicas malas generalmente tienen justificaciones que suenan bien cuando las tomas.
---
FAQ
¿Cuánto tiempo tardó la compilación inicial en ponerse en vivo?
Siete meses desde el primer commit hasta una versión con la que me sentía cómodo llamando v1. La primera versión pública áspera estuvo en línea alrededor del mes cuatro, pero tenía problemas serios de contenido delgado y la sección de comparación era prácticamente inútil. Diría que cuatro meses para "técnicamente en línea" y otros tres para "realmente bueno".
¿Usarías App Router si empezaras hoy?
Probablemente sí, para nuevos proyectos iniciados a finales de 2023 en adelante. Los componentes de servidor de App Router serían realmente adecuados para este tipo de generación de páginas con mucho contenido de datos. Pero migrar una aplicación existente de Pages Router de 25,000 páginas no es un proyecto que vaya a emprender en el corto plazo. Pages Router aún funciona, y que "funcione" es algo subestimado.
¿Cómo manejas proveedores que cierran o cambian significativamente su oferta?
Tenemos un flag de estado en la base de datos — activo, deprecado, redirigido. Los proveedores deprecados obtienen una página de archivo reducida en lugar de una eliminación completa, lo que preserva cualquier backlink. Los proveedores redirigidos (por ejemplo, cuando un host adquiere otro) obtienen un 301 manejado a través de la configuración de redirects de Next.js en next.config.js. Revisamos los flags de estado mensualmente.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é usarías en lugar de Next.js si lo hicieras de nuevo?
Honestamente no sé. Astro es interesante para sitios de contenido mayormente estático, y he estado jugando con él en un proyecto más pequeño. Pero Next.js nos dio la flexibilidad de tener secciones tanto estáticas como dinámicas en la misma base de código, lo que importaba. Para un directorio puramente estático sin características interactivas, Astro podría ser más rápido de compilar y más barato de ejecutar. Pregúntame de nuevo en un año.
¿Cómo evitas que los scrapers copien todo el directorio?
¿Honestamente? No puedes, completamente. Limitamos la velocidad de las rutas de API, usamos la gestión de bots de Cloudflare en el frontend, y rotamos algunos de los datos estructurados para que las copias scrapeadas se vuelvan obsoletas rápidamente. Pero si alguien quiere clonar un directorio de acceso público, va a encontrar la manera. El diferencial competitivo es la actualización de datos y la calidad de UX, no la ofuscación técnica.
---
Pensamiento Final
HostList no es un éxito arrollador. Genera dinero —comisiones de afiliado, algunos acuerdos de publicidad directa— y posiciona razonablemente bien para quizás 600 de los términos que originalmente busqué. Está bien. Fue un proyecto de aprendizaje que además genera ingresos, que es lo mejor que puede pasar.
Si estás pensando en construir un sitio SEO programático a gran escala en Next.js, mi consejo honesto es este: hazlo. Es genuinamente un stack excelente para el trabajo. Pero construye menos de lo que crees necesitar, construyelo mejor de lo que piensas que tienes tiempo, y resuelve tu arquitectura de datos antes de escribir un solo template de página.
La tecnología es la parte fácil. Siempre lo es.
