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.jsse 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
useRefpara prevenir dupla inicialização do React StrictMode euseEffectpara montar o jogo - dashboard/page.js (481 linhas) — Gráficos de telemetria com Chart.js. Usa
useState/useEffectpara buscar dados viafetche 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):
- Home —
revalidate = 3600(1 hora). Mostra top 5 do ranking e patrocinadores, atualizado a cada hora - Ranking —
revalidate = 300(5 minutos). As 3 variações (geral, semanal, mensal) usam o mesmo intervalo - Stats diário —
revalidate = 120(2 minutos). O mais agressivo, pois dados diários mudam rapidamente - Stats mensal —
revalidate = 300(5 minutos). Intervalo moderado - Stats anual —
revalidate = 600(10 minutos). Dados mais estáveis - Patrocinadores —
revalidate = 600(10 minutos) - Perfis de jogador —
revalidate = 3600(1 hora) comdynamicParams = 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/scores —
GET(top 100 scores) +POST(registrar novo score com sanitização) - /api/anonymous-sessions —
GET(7 aggregations de sessões anônimas) +POST(registrar sessão com validação de type) - /api/sponsors —
GET(listar patrocinadores por valor) - /api/sponsors/click —
POST(registrar clique comObjectIdvalidation, incrementartotalClickse inserir emsponsor_clicks) - /api/stats —
GET(10 aggregations paralelas viaPromise.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/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: 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
$groupaggregation - 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
metadataestático ougenerateMetadata()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
BreadcrumbListautomaticamente + 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 sniffingX-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.
