how-i-built-25000-page-directory-nextjs.html
< BACK Imagen destacada para "Cómo construí un directorio de 25,000 páginas en Next.js: Post-mortem de HostList"

Cómo construí un directorio de 25,000 páginas en Next.js: Post-mortem de HostList

A finales de 2022 me convencí de que construir un directorio de hosting web sería directo. Agregar datos, generar páginas, rankear, monetizar. Limpio. Ya había hecho proyectos de SEO programático antes -- una herramienta de bienes raíces localizada para un cliente en UK, un sitio de comparación de SaaS que llegó a 40k visitas mensuales -- así que pensé que HostList sería un proyecto de seis semanas. Tomó casi 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.Vercel bill I hadn't budgeted for.

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 administrado -- 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 cuentas: ~400 proveedores × varios tipos de página × 20+ combinaciones de filtro. Llegas a 25,000 páginas más rápido de lo que pensarías.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.

Elegí Next.js casi sin pensarlo. En Seahawk lo usamos para la mayoría de nuestros proyectos más grandes basados en React. El ecosistema está maduro, getStaticProps y getStaticPaths tienen sentido para la generación estática pesada en SEO, y personalmente encuentro el enrutamiento basado en archivos más fácil de razonar que Remix o Gatsby a esta escala.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.

La primera decisión real fue la capa de datos. Descarté un CMS headless 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 lote de forma limpia. Nos decantamos por una base de datos Postgres en Supabase, con una capa API Next.js ligera enfrente. 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

Esto es lo que pasa con getStaticPaths y 25,000 rutas. Funciona. Técnicamente. Pero tus tiempos de compilación te harán cuestionar tus decisiones en la vida.getStaticPaths with 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 generalmente es solo "toma tiempo"). El problema real es la invalidación de caché. Cuando un proveedor de hosting actualiza sus precios (y lo hacen, constantemente), necesitas reconstruir páginas afectadas. Si todo está pre-renderizado estáticamente sin ISR, estás desencadenando reconstrucciones completas para cambios de datos que afectan tal vez 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 Incremental Static Regeneration con un revalidate de 86400 segundos (24 horas) para la mayoría de páginas, y 3600 segundos para páginas de proveedores con mucho contenido de precios. Este fue el mejoramiento más significativo de calidad de vida en 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 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'.

Dividiendo el Árbol de Rutas

Una cosa que haría diferente, y que ahora le digo a cada desarrollador en Seahawk que toque un proyecto programático grande: divide tu árbol de rutas temprano. No tengas una función monolítica getStaticPaths tratando de devolver 25,000 slugs. Dividimos la nuestra en:getStaticPaths function trying to return 25,000 slugs. We broke ours into:

  1. /providers/[slug] -- páginas de proveedor individual (~400) -- individual provider pages (~400)
  2. /compare/[slugA]-vs-[slugB] -- páginas de comparación cara a cara (~8,000) -- head-to-head comparison pages (~8,000)
  3. /category/[type] -- páginas de destino de categoría (~40) -- category landing pages (~40)
  4. /location/[country]/[type] -- combinaciones de geo × categoría (~16,000+) -- geo × category combinations (~16,000+)
  5. /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 dev 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 interfaz de administración interna adecuada. Eventualmente lo hicimos, usando Next.js API routes y un dashboard de Retool pegado 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 escribe en la base de datos. La interfaz de administración 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 activa cuando cambia una columna price_updated_at, golpeando un endpoint de revalidación Next.jsprice_updated_at column 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ó algo 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" tiene 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 directa al respecto, y debería haber sido más directo conmigo mismo antes.Google's own guidance on thin content is blunt about this, and I should have been blunter with myself earlier.

Qué Hicimos para Recuperarnos

Hicimos una depuración. 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 basándose en la superposición de categoría y ubicación. Sin intervención editorial necesaria. No es perfecto -- ocasionalmente una página de hosting VPS enlaza con algo un poco de lado -- pero es 90% correcto, 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

Pensarías que la generación estática haría que el rendimiento fuera fácil. Y a nivel conceptual, lo hace -- HTML pre-renderizado, almacenado en caché en el edge en el CDN de Vercel, sin sobrecarga de renderización del servidor. 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 proveedores como en páginas de comparación. En móvil, estaba causando un Largest Contentful Paint de alrededor de 4.8 segundos. Malo. Realmente malo para un sitio donde el tráfico principal son personas a mitad de camino en una decisión de compra.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.

La reconstruimos como una tabla estática renderizada en el servidor con una capa de hidratación React delgada para las partes de filtro interactivo. El LCP bajó a 1.9 segundos. Eso no es magia -- es solo hacer la cosa aburrida correctamente.

El problema de las imágenes

Cada proveedor tiene un logo. 400 logos, más capturas de pantalla, vistas previas de la interfaz, iconos de características. Cometimos el error de alojar estas en la optimización de imágenes integrada de Vercel durante los primeros dos meses. Los costos de ancho de banda fueron silenciosamente aterradores. Movimos todo a Cloudflare R2 con un dominio personalizado, bajamos nuestra factura de Vercel de £180/mes a £40/mes. Si estás construyendo algo pesado en imágenes, mira Cloudflare R2 temprano -- el egreso gratuito es genuinamente útil a escala.Cloudflare R2 early -- 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:

  1. Recopilación de datos -- scraper de Python en un cron job de Railway, escribe en Supabase Postgres -- Python scraper on a Railway cron job, writes to Supabase Postgres
  2. Capa de administración -- Dashboard de Retool para ediciones manuales, correcciones y marcas de proveedores -- Retool dashboard for manual edits, corrections, and provider flags
  3. Aplicación Next.js -- Pages router (comenzamos antes de que App Router fuera lo suficientemente estable para confiar), desplegada en Vercel -- Pages router (we started before App Router was stable enough to trust), deployed on Vercel
  4. ISR + revalidación bajo demanda -- aproximadamente 2,000 páginas principales precompiladas, el resto bajo demanda, todas con revalidación cada 24h -- top ~2,000 pages pre-built, rest on-demand, all with 24h revalidation
  5. Imágenes -- Cloudflare R2, servidas a través de un subdominio personalizado con CDN de Cloudflare al frente -- Cloudflare R2, served via a custom subdomain with Cloudflare CDN in front
  6. Análisis -- 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
  7. 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

  1. Empecé demasiado amplio. 25,000 páginas siempre fue el objetivo, pero debería haber lanzado con 500 páginas de alta calidad y expandido. En cambio, 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.
  2. No configuré la revalidación correctamente desde el primer día. Desperdiciamos dos meses en reconstrucciones completas que ISR habría hecho innecesarias.We wasted two months on full rebuilds that ISR would have made unnecessary.
  3. Mantuve la Hoja de Cálculo de Google. La única fuente de verdad debería haber sido no negociable desde la primera semana.Single source of truth should have been non-negotiable from week one.
  4. Subestimé la calidad de las páginas de comparación. El volumen no es una estrategia.Volume is not a strategy.
  5. Usamos optimización de imágenes de Vercel demasiado tiempo. Nos pasamos a R2 seis semanas más tarde de lo que debería haber sido.Moved to R2 six weeks later than we should have.
  6. No dividimos el árbol de rutas lo suficientemente temprano. Mezclamos rutas rápidas y lentas en la misma llamada a getStaticPaths y luego nos preguntábamos por qué las compilaciones eran lentas.Mixed fast and slow routes in the same getStaticPaths call 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 malas decisiones arquitectónicas suelen tener 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 una bandera de estado en la base de datos -- active, deprecated, redirected. Los proveedores deprecated reciben una página de archivo delgada en lugar de una eliminación completa, lo que preserva cualquier backlink. Los proveedores redirected (por ejemplo, cuando un host adquiere otro) reciben un 301 manejado a través de la configuración de redirects de Next.js en next.config.js. Revisamos las banderas de estado mensualmente.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.

¿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.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.

¿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 afiliados, algunos acuerdos publicitarios directos -- y ocupa un lugar razonablemente bueno para quizás 600 de los términos que originalmente me propuse clasificar. 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.

< BACK