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: 62+ Páginas com 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 (7 funções de consulta), blogArticles.js (conteúdo dos 23 artigos), adminAuth.js (validação de senha)
  • src/components/ — 5 componentes reutilizáveis: Header, Footer, Breadcrumb, RankingTable, JsonLd

O projeto gera 62+ páginas estáticas e dinâmicas: 12 páginas estáticas (home, play, ranking, blog index, stats, sponsors, etc.), 23 artigos de blog via SSG, 500+ perfis de jogador via ISR, e 4 variações de stats/ranking.

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: de todas as páginas do projeto, apenas 3 são Client Components (marcadas com 'use client'):

  • play/page.js (1.314 linhas) — 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
  • dashboard/page.js (481 linhas) — Gráficos de telemetria com Chart.js. Usa useState/useEffect para buscar dados via fetch e renderizar Line, Bar e Doughnut charts
  • admin/page.js (722 linhas) — Painel administrativo com CRUD de scores e patrocinadores

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 a página do jogo, dashboard e admin 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):

  • Homerevalidate = 3600 (1 hora). Mostra top 5 do ranking e patrocinadores, atualizado a cada hora
  • Rankingrevalidate = 300 (5 minutos). As 3 variações (geral, semanal, mensal) usam o mesmo intervalo
  • Stats diáriorevalidate = 120 (2 minutos). O mais agressivo, pois dados diários mudam rapidamente
  • Stats mensalrevalidate = 300 (5 minutos). Intervalo moderado
  • Stats anualrevalidate = 600 (10 minutos). Dados mais estáveis
  • Patrocinadoresrevalidate = 600 (10 minutos)
  • Perfis de jogadorrevalidate = 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 13 Handlers HTTP

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

  • /api/scoresGET (top 100 scores) + POST (registrar novo score com sanitização)
  • /api/anonymous-sessionsGET (7 aggregations de sessões anônimas) + POST (registrar sessão com validação de type)
  • /api/sponsorsGET (listar patrocinadores por valor)
  • /api/sponsors/clickPOST (registrar clique com ObjectId validation, incrementar totalClicks e inserir em sponsor_clicks)
  • /api/statsGET (10 aggregations paralelas via Promise.all: total de jogos, scores, pilotos únicos, ativos em 7 dias, jogos/dia, horários de pico, tiers, top 10, recentes, distribuição)
  • /api/admin/scoresGET + DELETE + PUT (CRUD protegido por senha)
  • /api/admin/sponsorsGET + POST + PUT + DELETE (CRUD completo protegido)
  • /api/admin/sponsors/statsGET (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:

  • scoresScores registrados com nome de piloto. Campos: name, score, tier, tierName, boosts, spread, time, difficulty, playedAt
  • anonymous_sessionsSessões anônimas. Campos: type (Unregistered/Unknown), score, tier, tierName, boosts, spread, time, difficulty, playedAt
  • sponsorsPatrocinadores. 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: 7 Funções de Consulta em data.js

O arquivo src/lib/data.js (284 linhas) centraliza todas as consultas ao banco de dados em funções reutilizáveis:

  • getTopScores(limit) — Top scores ordenados por score decrescente, com serialização de _id
  • getScoresByPeriod(period, limit) — Scores filtrados por período: 'weekly' (7 dias), 'monthly' (30 dias) ou 'all'
  • 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
  • getAggregatedStats(period) — 9 aggregations paralelas combinando scores + anonymous_sessions: totais, médias, distribuição de tiers, jogos/dia, top jogadores, recentes
  • 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: 12 URLs estáticas com prioridades (home=1.0, play=0.9, ranking=0.8), 23 slugs de blog, e até 1.000 jogadores consultados diretamente do MongoDB via distinct('playerName')
  • 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 4 arquivos CSS puros:

  • globals.css — Estilos base, variáveis CSS (cores, espaçamentos), tipografia, layout responsivo, componentes globais (botões, cards, tabelas, grids)
  • game.css — Estilos específicos do jogo: canvas, HUD overlay, tela de game over, modal de registro
  • dashboard.css — Layout dos gráficos de telemetria e cards de métricas
  • 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 no dashboard (client component). O jogo inteiro, o ranking, o blog, os stats — 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 importa clientPromise diretamente e consulta db.collection('scores').distinct('playerName') 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.