O Navistron é construído com Next.js 15 e React 19 usando o App Router — a arquitetura moderna do framework. Mas o que torna a estrutura interessante não é a complexidade: é a simplicidade radical. O projeto inteiro funciona com apenas 6 dependências de produção (next, react, react-dom, mongodb, chart.js, react-chartjs-2), sem ORM, sem framework CSS, sem biblioteca de autenticação. Neste artigo, vamos explorar cada camada dessa arquitetura.

Visão Geral: Dezenas de Páginas em 3 Diretórios

A estrutura segue a convenção do App Router do Next.js com três diretórios principais:

  • src/app/ — Todas as páginas, layouts e API Routes. Cada subdiretório com um page.js se torna uma rota automaticamente
  • src/lib/ — Utilitários de acesso a dados: mongodb.js (conexão singleton), data.js (ranking, perfis e patrocinadores), stats.js + statsFilters.js (telemetria unificada e filtros), blogArticles.js (conteúdo dos 23 artigos), adminAuth.js (validação de senha)
  • src/components/ — 7 componentes reutilizáveis: Header, Footer, Breadcrumb, RankingTable, JsonLd, e os dois Client Components da telemetria, StatsFilters e StatsCharts

O projeto gera dezenas de páginas estáticas e dinâmicas: páginas estáticas (home, play, ranking, blog index, sponsors), 23 artigos de blog via SSG, 500+ perfis de jogador via ISR, e a página de telemetria /stats, renderizada sob demanda conforme os filtros da URL.

Server Components vs Client Components: A Divisão Estratégica

No Next.js 15, todo componente é Server Component por padrão. O Navistron leva isso ao extremo: apenas 2 páginas são Client Components (marcadas com 'use client'), mais dois componentes pequenos dentro da telemetria:

  • play/page.js — O engine do jogo inteiro: Canvas 2D, game loop, input, partículas, HUD. Usa useRef para prevenir dupla inicialização do React StrictMode e useEffect para montar o jogo
  • admin/page.js — Painel administrativo com CRUD de scores e patrocinadores
  • components/StatsFilters.js e components/StatsCharts.js — A barra de filtros (que só monta a URL e navega com useRouter) e os gráficos Chart.js da página /stats. A página em si é um Server Component que lê searchParams e consulta o banco

Todo o resto — home, ranking, blog, stats, sponsors, perfis de jogador — são Server Components puros. Isso significa que o JavaScript deles nunca é enviado ao navegador. O resultado é um bundle mínimo: apenas o jogo, o admin e os dois componentes interativos da telemetria carregam JS no client.

Os 5 componentes compartilhados (Header, Footer, Breadcrumb, RankingTable, JsonLd) também são Server Components — o menu hamburger mobile, por exemplo, funciona com CSS puro (checkbox + label trick), sem nenhum JavaScript.

ISR e SSG: Estratégia de Revalidação por Página

O Navistron usa diferentes tempos de revalidação por tipo de conteúdo, combinando ISR (Incremental Static Regeneration) e SSG (Static Site Generation):

  • Home — revalidate = 3600 (1 hora). Mostra top 5 do ranking e patrocinadores, atualizado a cada hora
  • Ranking — revalidate = 300 (5 minutos) para o top 100
  • Telemetria (/stats) — Renderização dinâmica, pois os filtros vêm da URL. Em vez de ISR, o resultado de cada combinação de filtros é cacheado por 2 minutos com unstable_cache, e o /api/stats responde com s-maxage=120
  • Patrocinadores — revalidate = 600 (10 minutos)
  • Perfis de jogador — revalidate = 3600 (1 hora) com dynamicParams = true
  • Blog — SSG puro via generateStaticParams(), sem revalidação (rebuild only)

Os artigos de blog usam generateStaticParams() que chama getAllSlugs() para pré-renderizar todas as 23 páginas em build time. Os perfis de jogador usam o mesmo mecanismo, pré-renderizando até 500 jogadores via getAllPlayerNames().slice(0, 500) — jogadores além dos 500 são renderizados on-demand graças ao dynamicParams = true.

8 API Routes com 12 Handlers HTTP

O Navistron implementa uma API REST interna usando Route Handlers do App Router. São 8 arquivos de rota com 12 handlers no total:

  • /api/scores — GET (top 100 scores) + POST (registrar novo score com sanitização)
  • /api/anonymous-sessions — POST (registrar sessão anônima com validação de type)
  • /api/sponsors — GET (listar patrocinadores por valor)
  • /api/sponsors/click — POST (registrar clique com ObjectId validation, incrementar totalClicks e inserir em sponsor_clicks)
  • /api/stats — GET (telemetria unificada em JSON, aceitando os mesmos filtros da página /stats: período, tipo, tier, piloto, visão, ordenação e página)
  • /api/admin/scores — GET + DELETE + PUT (CRUD protegido por senha)
  • /api/admin/sponsors — GET + POST + PUT + DELETE (CRUD completo protegido)
  • /api/admin/sponsors/stats — GET (7 aggregations de analytics de cliques)

Rotas admin usam validateAdmin(password) — uma comparação simples contra process.env.NAVISTRON_PASSWORD. Sem JWT, sem cookies de sessão, sem middleware — a senha é enviada por requisição.

MongoDB: Singleton Pattern e 4 Collections

A conexão com o MongoDB usa o padrão singleton em src/lib/mongodb.js. A função getClientPromise() verifica se já existe uma Promise de conexão em cache; em desenvolvimento, armazena em global._mongoClientPromise para sobreviver ao Hot Module Replacement (HMR) do Next.js — sem isso, cada reload criaria uma nova conexão.

O banco navistron possui 4 collections:

  • scores — Scores registrados com nome de piloto. Campos: name, score, tier, tierName, boosts, spread, time, difficulty, playedAt
  • anonymous_sessions — Sessões anônimas. Campos: type (Unregistered/Unknown), score, tier, tierName, boosts, spread, time, difficulty, playedAt
  • sponsors — Patrocinadores. Campos: name, link, linkText, value, totalClicks, createdAt
  • sponsor_clicks — Eventos de clique. Campos: sponsorId, clickedAt, userAgent, referer

O driver nativo mongodb ^5.9.2 é usado diretamente — sem Mongoose, Prisma ou outro ORM. Todas as queries usam a API nativa do driver: find(), insertOne(), updateOne(), deleteOne(), aggregate(), e distinct().

Data Layer: data.js e stats.js

O arquivo src/lib/data.js centraliza as consultas de ranking, perfil e patrocinadores; src/lib/stats.js concentra toda a telemetria:

  • getTopScores(limit) — Top scores ordenados por score decrescente, excluindo registros anônimos, com serialização de _id
  • getAllPlayerNames() — Nomes únicos de pilotos via $group aggregation
  • getPlayerStats(nickname) — Perfil completo: best/avg score, total de jogos, tempo total, rank global (count de jogadores com score superior), top 20 scores do jogador
  • getSponsors() — Patrocinadores ordenados por valor
  • getStats(filters) (stats.js) — Une scores + anonymous_sessions com $unionWith e devolve resumo, gráficos, ranking por piloto ou lista de partidas (ordenados e paginados) e partidas recentes, cacheado por 2 minutos
  • parseStatsFilters / statsHref (statsFilters.js) — Validação dos parâmetros de URL e construção de links, compartilhados entre servidor e cliente
  • formatTime(s) / formatValue(cents) / siteUrl(path) — Utilitários de formatação

Cada função chama getClientPromise(), acessa client.db('navistron') e executa a query. O pattern é consistente: todas são async, todas retornam dados serializados (ObjectIds convertidos para strings), e todas tratam a conexão de forma lazy (só conecta quando chamada).

SEO Nativo: Metadata, JSON-LD e Sitemap Dinâmico

O Navistron implementa SEO através de mecanismos nativos do Next.js, sem plugins:

  • Metadata API — Cada página exporta metadata estático ou generateMetadata() dinâmico com title, description, keywords, canonical, OpenGraph (1200×630), Twitter Cards
  • Template de título — O root layout define title.template: '%s | Navistron', herdado por todas as páginas filhas
  • JSON-LD — 10+ tipos de schema via componente JsonLd: VideoGame, SoftwareApplication, Organization, FAQPage (auto-extraído do HTML), Article, Person, Dataset, Blog, BreadcrumbList, WebPage, ItemList
  • sitemap.js — Sitemap dinâmico gerado por função: URLs estáticas com prioridades (home=1.0, play=0.9, ranking e stats=0.8), 23 slugs de blog, e até 1.000 jogadores obtidos via getAllPlayerNames()
  • robots.js — Permite /, bloqueia /api/ e /admin/, referencia o sitemap
  • Breadcrumbs — Componente gera JSON-LD BreadcrumbList automaticamente + markup semântico com <nav aria-label="Breadcrumb">

Os artigos de blog têm um recurso especial: o generateMetadata usa type: 'article' no OpenGraph com publishedTime, e o JSON-LD FAQPage é auto-gerado via regex que extrai pares <h3>...</h3><p>...</p> do HTML do artigo.

CSS Puro: Sem Tailwind, Sem CSS-in-JS

O Navistron não usa nenhum framework CSS. Toda a estilização vem de 3 arquivos CSS puros:

  • globals.css — Estilos base, variáveis CSS (cores, espaçamentos), tipografia, layout responsivo, componentes globais (botões, cards, tabelas, grids) e a seção de telemetria: barra de filtros, cards de gráficos, tabelas ordenáveis e paginação
  • game.css — Estilos específicos do jogo: canvas, HUD overlay, tela de game over, modal de registro
  • admin.css — Painel administrativo: formulários, tabelas editáveis, modais

O menu hamburger mobile é implementado com o checkbox hack — um <input type="checkbox"> escondido, um <label> como botão, e seletores CSS :checked ~ nav para abrir/fechar. Zero JavaScript para a navegação.

Segurança: Headers, Validação e Admin

O next.config.mjs define 3 security headers aplicados a todas as rotas:

  • X-Content-Type-Options: nosniff — Previne MIME type sniffing
  • X-Frame-Options: SAMEORIGIN — Previne iframe embedding (clickjacking)
  • Referrer-Policy: strict-origin-when-cross-origin — Controla informação enviada no header Referer

O header X-Powered-By é desabilitado via poweredByHeader: false. Nas API Routes, a sanitização é dupla: o client valida antes de enviar (nome uppercase, max 20 chars) e o servidor revalida tudo com Number(), defaults seguros e .toUpperCase().slice(0, 20). Rotas admin são protegidas por validateAdmin() que compara a senha contra process.env.NAVISTRON_PASSWORD.

Deploy: Vercel com Zero Config

O Navistron é deployed na Vercel — a plataforma criada pela mesma equipe do Next.js. Isso significa suporte nativo a Server Components, ISR, API Routes serverless, edge network para assets estáticos, e HTTPS automático. O deploy é feito via Git: cada push para o branch principal gera um novo build. MONGODB_URI e NAVISTRON_PASSWORD são configurados como variáveis de ambiente na plataforma.

As imagens são servidas via next/image com otimização automática da Vercel: conversão para WebP, lazy loading (exceto hero images com priority), e dimensões explícitas para evitar CLS (Cumulative Layout Shift). O NEXT_PUBLIC_SITE_URL define o domínio base, com fallback para navistron.io.

FAQ — Perguntas Frequentes sobre a Arquitetura

O jogo é server-rendered?

O HTML da página do jogo é server-rendered (o container, o HUD, os botões), mas toda a lógica do Canvas é executada no client via 'use client'. O useEffect chama initGame() que configura o Canvas 2D, registra event listeners e inicia o game loop com requestAnimationFrame.

Por que o driver nativo do MongoDB e não um ORM?

O projeto prioriza zero abstração desnecessária. O driver nativo mongodb ^5.9.2 permite queries MongoDB exatas — aggregation pipelines com $group, $match, $bucket, $dateToString, $lookup — sem a limitação de query builders. Com apenas 4 collections e consultas bem definidas, um ORM adicionaria complexidade sem benefício claro.

Como o blog funciona sem CMS?

Os 23 artigos estão definidos num array JavaScript em blogArticles.js. Cada artigo é um objeto com slug, title, description, keywords, category, date, heroImage e content (HTML). Funções utilitárias fornecem getAllSlugs(), getArticleBySlug(), getArticlesByCategory() e getRelatedArticles(). O generateStaticParams() pré-renderiza todos em build time. Sem banco de dados para conteúdo editorial.

Quantas dependências o projeto usa?

Apenas 6 de produção: next ^15.1.0, react ^19.0.0, react-dom ^19.0.0, mongodb ^5.9.2, chart.js ^4.4.7 e react-chartjs-2 ^5.2.0. As duas últimas são usadas exclusivamente nos gráficos da página de telemetria (componente StatsCharts). O jogo inteiro, o ranking, o blog — tudo funciona com zero dependências além do Next.js e React.

Como o sitemap inclui jogadores do banco de dados?

O sitemap.js chama getAllPlayerNames() do data layer, que agrupa a collection scores por name (ignorando registros anônimos), para obter todos os nomes únicos de pilotos. Cada nome gera uma URL /player/{nome} com prioridade 0.5 e changeFrequency: 'daily', limitado a 1.000 jogadores. Isso garante que perfis de jogadores são indexados pelo Google automaticamente.