Lá por final de 2022 me convenci de que construir um diretório de hospedagem web seria tranquilo. Agregar dados, gerar páginas, rankear, monetizar. Simples. Eu já tinha feito SEO programático antes -- uma ferramenta de imóveis localizada para um cliente do Reino Unido, um site de comparação de SaaS que chegou a 40 mil visitas por mês -- então achei que HostList seria um projeto de seis semanas. Levou perto de sete meses. E quase quebrou algumas coisas: meu horário de sono, a confiança de um dos meus devs juniores, e uma conta de £180/mês no Vercel que eu não tinha orçado.Vercel bill I hadn't budgeted for.
Este é o post-mortem. O real, não a versão do LinkedIn.
---
O Brief que Escrevi para Mim Mesmo
HostList era para ser simples. Um diretório de provedores de hospedagem web -- compartilhada, VPS, dedicada, WordPress gerenciado -- com páginas individuais para cada provedor, páginas de comparação, páginas de categoria, e páginas baseadas em localização (p.ex. "melhor hospedagem na Alemanha"). Faça as contas: ~400 provedores × vários tipos de página × mais de 20 combinações de filtro. Você chega a 25 mil páginas mais rápido do que pensa.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.
Escolhi Next.js quase sem pensar. Usamos na Seahawk para a maioria dos nossos builds maiores baseados em React. O ecossistema é maduro, getStaticProps e getStaticPaths fazem sentido para geração estática pesada em SEO, e pessoalmente acho o roteamento baseado em arquivos mais fácil de raciocinar do que Remix ou Gatsby nessa 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.
A primeira decisão real foi a camada de dados. Descartei uma headless CMS bem rápido -- não queria pagar taxas de Contentful para 25 mil entradas, e não confiava em uma CMS para lidar com escritas programáticas em massa de forma limpa. Acabamos em um banco de dados Postgres no Supabase, com uma camada leve de API Next.js na frente. Essa parte funcionou bem. É quase tudo o mais que ficou complicado.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.
---
Geração Estática em Escala: O Que Ninguém Te Avisa
Aqui está o problema com getStaticPaths e 25.000 rotas. Funciona. Tecnicamente. Mas seus tempos de build vão te fazer questionar suas escolhas de vida.getStaticPaths with 25,000 routes. It works. Technically. But your build times will make you question your life choices.
Nossa primeira build completa levou 4 horas e 47 minutos. No Vercel. O que, se você não tiver cuidado com seus limites de plano, é o tipo de coisa que causa uma notificação de cobrança às 2 da manhã. Fiquei olhando para aquele alerta do Slack do meu celular e genuinamente considerei só usar WordPress.
A Armadilha do `fallback: 'blocking'`
Meu instinto inicial era pré-renderizar tudo. Toda página, toda combinação. Má ideia -- e não pela razão que a maioria dos tutoriais te avisa (que é geralmente só "demora um tempo"). O problema real é invalidação de cache. Quando um provedor de hospedagem atualiza seu preço (e atualiza, constantemente), você precisa reconstruir as páginas afetadas. Se tudo é pré-renderizado estaticamente sem ISR, você está acionando reconstruções completas para mudanças de dados que afetam talvez 30 páginas em 25 mil.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.
Passei para Incremental Static Regeneration com um revalidate de 86400 segundos (24 horas) para a maioria das páginas, e 3600 segundos para páginas de provedores pesadas em preços. Essa foi a maior melhoria de qualidade de vida em todo o projeto. Os tempos de build caíram para menos de 40 minutos porque estávamos pré-renderizando apenas as ~2.000 páginas principais por prioridade de tráfego e deixando o resto gerar sob demanda com 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'.
Dividindo a Árvore de Rotas
Uma coisa que eu faria diferente, e que falo para todo dev na Seahawk que toca um projeto programático grande agora: divida sua árvore de rotas cedo. Não tenha uma função getStaticPaths monolítica tentando retornar 25.000 slugs. Dividimos a nossa em:getStaticPaths function trying to return 25,000 slugs. We broke ours into:
/providers/[slug] -- páginas individuais de provedor (~400)-- individual provider pages (~400)/compare/[slugA]-vs-[slugB] -- páginas de comparação frente a frente (~8 mil)-- head-to-head comparison pages (~8,000)/category/[type] -- páginas de destino de categoria (~40)-- category landing pages (~40)/location/[country]/[type] -- combinações de geo × categoria (~16 mil+)-- geo × category combinations (~16,000+)/best/[use-case] -- páginas de lista curada (~600)-- curated list pages (~600)
Cada grupo de rotas tem seu próprio cadência de revalidação, sua própria lógica de busca de dados, e criticamente, sua própria prioridade de build. As páginas de localização são quase inteiramente sob demanda. As páginas de provedor são sempre pré-renderizadas. Separação clara.
---
A Bagunça do Pipeline de Dados (E Como Consertamos)
No início de 2023 cometi o erro de construir o lado de coleta de dados do HostList muito frouxamente. Tínhamos um script de scraping (escrito em Python, usando BeautifulSoup e um pool de proxy rotativo do Webshare), uma Google Sheet manual para correções, e uma tabela Supabase. Três fontes de verdade. Nenhuma delas conversando adequadamente uma com a outra.
Um dev júnior -- gente boa, saído de um bootcamp -- gastou três semanas mantendo um script de sincronização entre a Sheet e Supabase que quebrava toda vez que um nome de coluna mudava. Eu deveria ter matado a Sheet na primeira semana e construído uma admin UI interna de verdade. Eventualmente fizemos, usando Next.js API routes e um dashboard Retool grudado na lateral, mas queimamos provavelmente 60 horas de engenharia para chegar lá.
A solução: uma única fonte da verdade, sempre. O banco de dados é canônico. Tudo escreve no banco de dados. A interface de admin lê e escreve no banco de dados. O scraper escreve no banco de dados. Parece óbvio. Sempre parece, olhando para trás.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.
Mantendo Dados Frescos em Escala
Para um diretório deste tamanho, a atualização de dados é uma preocupação de SEO tanto quanto de UX. Google percebe quando tabelas de preço mostram £2.99/mês para um plano que custa £5.99 há oito meses. Configuramos:
- Um job de scrape semanal rodando em um cron Railway (barato, confiável, não requer um servidor dedicado)
- Um webhook do banco de dados Supabase que dispara quando uma coluna price_updated_at muda, acionando um endpoint de revalidação Next.js
price_updated_atcolumn changes, hitting a Next.js revalidation endpoint - Flags de override manual no Retool para os ~30 provedores cujos sites bloqueiam ativamente scrapers
Esse endpoint de revalidação -- /api/revalidate?secret=TOKEN&path=/providers/siteground -- é um recurso padrão do Next.js, mas conectar ele a um webhook de banco de dados deu trabalho. Valeu 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.
---
Arquitetura de SEO: O Que Realmente Mexeu a Agulha
Construí sites de conteúdo o suficiente para saber que ter 25 mil páginas não é a mesma coisa que ter 25 mil páginas que ranqueiam. As páginas de comparação foram a armadilha. Geramos todas as combinações A-vs-B possíveis para nossos ~400 provedores, o que nos deu aproximadamente 79.800 emparelhamentos teóricos. Construímos ~8.000 deles. E a maioria deles era, francamente, fina demais.
Confissão honesta: fiquei ganancioso. A lógica de SEO era sólida -- "SiteGround vs Bluehost" tem volume de busca real, a cauda longa de queries de comparação é enorme -- mas não construímos conteúdo único o suficiente por página para justificar a existência de cada uma. Google começou a rastrear a seção de comparações e claramente decidiu que não valia a pena. A própria orientação do Google sobre thin content é direta sobre isso, e eu deveria ter sido mais direto comigo mesmo mais cedo.Google's own guidance on thin content is blunt about this, and I should have been blunter with myself earlier.
O Que Fizemos para Recuperar
A gente cortou. Reduziu as páginas de comparação de ~8.000 para ~1.200 -- só pares com volume de busca demonstrável (verificado no Ahrefs, mínimo de 50 buscas mensais globais). Depois enriquecemos as páginas restantes com:
- Seções dinâmicas "para quem é melhor" extraídas de dados estruturados do provedor
- Dados reais de uptime (integramos com uma API de uptime de terceiros)
- Resumos de avaliações de usuários extraídos de dados do Trustpilot quando disponíveis
O resultado foi 1.200 páginas que eram realmente úteis em vez de 8.000 páginas que não eram. O tráfego orgânico para a seção de comparação aumentou 340% nos três meses seguintes. Contraintuitivo até não ser.
Link Interno em Escala
Com 25.000 páginas, linking interno não pode ser manual. A gente construiu um componente de páginas relacionadas que consulta Supabase em tempo de build (em getStaticProps) e retorna as cinco páginas adjacentes mais relevantes baseado em sobreposição de categoria e localização. Nenhuma intervenção editorial necessária. Não é perfeito -- ocasionalmente uma página de VPS hosting linka para algo meio de lado -- mas está 90% correto, e significou que toda página tinha links internos contextualmente relevantes desde o dia um.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: A Parte Que Te Humilha
Você pensaria que geração estática tornaria performance fácil. E em nível conceitual, torna -- HTML pré-renderizado, edge-cached na CDN do Vercel, sem overhead de server-rendering. Mas 25.000 páginas significa 25.000 oportunidades de ter tomado uma decisão ruim sobre sua árvore de componentes.
Nosso maior problema de performance era a tabela de comparação de providers. Era um componente React pesado do lado do cliente -- muito state, muito conditional rendering, usado tanto em páginas de provider quanto em páginas de comparação. Em mobile, estava causando um Largest Contentful Paint de cerca de 4.8 segundos. Ruim. Realmente ruim para um site onde o tráfego primário é pessoas no meio de uma decisão 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.
A gente reconstruiu como uma tabela estática server-rendered com uma camada fina de hidratação React para as partes de filtro interativas. LCP caiu para 1.9 segundos. Isso não é mágica -- é só fazer a coisa chata corretamente.
O Problema de Imagens
Todo provider tem um logo. 400 logos, mais screenshots, UI previews, ícones de features. A gente cometeu o erro de hospedar tudo na otimização de imagem built-in do Vercel nos primeiros dois meses. Os custos de bandwidth foram silenciosamente horríveis. Movemos tudo para Cloudflare R2 com um domínio customizado, caiu nossa conta do Vercel de £180/mês para £40/mês. Se você está construindo algo pesado em imagens, olhe Cloudflare R2 cedo -- o egress gratuito é genuinamente útil em escala.Cloudflare R2 early -- the free egress is genuinely useful at scale.
---
Como o Build Pipeline Realmente Se Parece Agora
Para qualquer um que queira o quadro concreto:
- Coleta de dados -- scraper Python em um cron job do Railway, escreve em Supabase Postgres -- Python scraper on a Railway cron job, writes to Supabase Postgres
- Camada admin -- painel Retool para edições manuais, correções e sinalizações de providers -- Retool dashboard for manual edits, corrections, and provider flags
- Aplicativo Next.js -- Pages router (começamos antes do App Router ser estável o suficiente para confiar), implantado no Vercel -- Pages router (we started before App Router was stable enough to trust), deployed on Vercel
- ISR + revalidação sob demanda -- cerca de 2.000 páginas top pré-construídas, resto sob demanda, todas com revalidação a cada 24h -- top ~2,000 pages pre-built, rest on-demand, all with 24h revalidation
- Imagens -- Cloudflare R2, servidas via um subdomínio customizado com CDN Cloudflare na frente -- Cloudflare R2, served via a custom subdomain with Cloudflare CDN in front
- Analytics -- Plausible para dados de tráfego respeitosos da privacidade, Ahrefs para rastreamento de rankings -- Plausible for privacy-friendly traffic data, Ahrefs for ranking tracking
- Monitoramento de uptime -- BetterUptime observando os cinco tipos de página com mais tráfego -- BetterUptime watching the five most traffic-heavy page types
Não é glamouroso. Também é em grande parte entediante de manter, que é exatamente o que você quer de uma infraestrutura que vai deixar rodando por três anos.
---
Erros Honestos, Numerados
- Comecei muito amplo. 25.000 páginas era sempre o objetivo, mas deveria ter lançado com 500 páginas de alta qualidade e expandido. Em vez disso, lancei com tudo e tive um problema de crawl budget do Google pelos primeiros quatro 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.
- Não configurei a revalidação corretamente desde o primeiro dia. Desperdicei dois meses em rebuilds completos que o ISR teria tornado desnecessários.We wasted two months on full rebuilds that ISR would have made unnecessary.
- Mantive a Planilha Google. A fonte única de verdade deveria ter sido inegociável desde a primeira semana.Single source of truth should have been non-negotiable from week one.
- Subestimei a qualidade da página de comparação. Volume não é uma estratégia.Volume is not a strategy.
- Usamos otimização de imagens do Vercel por muito tempo. Migramos para R2 seis semanas depois do que deveríamos ter feito.Moved to R2 six weeks later than we should have.
- Não dividimos a árvore de rotas cedo o suficiente. Misturamos rotas rápidas e lentas na mesma chamada getStaticPaths e depois ficamos perguntando por que as builds eram lentas.Mixed fast and slow routes in the same
getStaticPathscall and then wondered why builds were slow.
Cada uma dessas é uma decisão que parecia razoável na época. É essa a parte que tutoriais não capturam -- decisões arquitetônicas ruins geralmente têm justificativas que soam bem quando você as toma.
---
FAQ
Quanto tempo levou o build inicial para ir ao vivo?
Sete meses do primeiro commit até uma versão que eu me sentia confortável chamando de v1. A primeira versão pública bruta saiu no mês quatro, mas tinha sérios problemas de conteúdo fino e a seção de comparação era praticamente inútil. Eu diria quatro meses para "tecnicamente ao vivo" e mais três para "realmente bom".
Você usaria o App Router se estivesse começando hoje?
Provavelmente sim, para novos projetos iniciados no final de 2023 em diante. Os server components do App Router seriam realmente bem adequados para esse tipo de geração de página com muitos dados. Mas migrar um app Pages Router existente de 25.000 páginas não é um projeto que vou abraçar em breve. O Pages Router ainda funciona, e "funciona" é subestimado.
Como você lida com provedores que saem do negócio ou mudam sua oferta significativamente?
Temos uma flag de status no banco de dados -- active, deprecated, redirected. Providers deprecated ganham uma página de arquivo enxuta em vez de uma remoção completa, o que preserva qualquer backlink. Providers redirected (por exemplo, quando um host adquire outro) recebem um 301 tratado via configuração de redirects do Next.js em next.config.js. Revisamos as flags de status mensalmente.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.
O que você usaria em vez do Next.js se estivesse fazendo isso novamente?
Honestamente, não sei. Astro é interessante para sites de conteúdo praticamente estático, e tenho explorado em um projeto menor. Mas Next.js nos deu a flexibilidade de ter seções estáticas e dinâmicas no mesmo codebase, o que importava. Para um diretório puramente estático sem recursos interativos, Astro poderia ser mais rápido de buildar e mais barato de executar. Me pergunte novamente em um ano.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.
Como você impede que scrapers copiem todo o diretório?
Honestamente? Você não consegue, completamente. Fazemos rate-limit nas rotas de API, usamos o bot management do Cloudflare no frontend, e rotacionamos alguns dos dados estruturados para que cópias feitas por scraping fiquem obsoletas rapidamente. Mas se alguém quer clonar um diretório público, vai encontrar uma forma. O moat é atualização de dados e qualidade de UX, não ofuscação técnica.
---
Pensamento Final
HostList não é um sucesso arrebatador. Gera receita -- comissões de afiliados, alguns acordos de publicidade direta -- e faz rank razoavelmente bem para talvez 600 dos termos que originalmente tinha como alvo. Tudo bem. Foi um projeto de aprendizado que também gera receita, o que é o melhor tipo de coisa.
Se você está pensando em construir um site de SEO programático em larga escala em Next.js, meu conselho honesto é este: faça. É genuinamente uma boa stack para o trabalho. Mas construa menos do que você pensa que precisa, construa melhor do que você pensa que tem tempo para fazer, e organize sua arquitetura de dados antes de escrever um único template de página.
A tecnologia é a parte fácil. Sempre é.
