Guia para qualquer agente (Claude e afins) trabalhando neste repositório. Leia antes de editar. Documentação para humanos fica no README e no CONTRIBUTING.
Roadmap DSA — o maior guia visual e gratuito de Algoritmos e Estruturas de Dados em português, feito pela comunidade Craft & Code Club. Cada tópico tem: algoritmo rodando passo a passo (visualizador), artigo, vídeo, problemas (LeetCode/GeeksforGeeks) e referências.
- Domínio:
https://dsa.craftcodeclub.io. Repo:craft-code-club/roadmap-dsa. - Licença: dupla. Código sob MIT (open source, uso comercial permitido); conteúdo
didático sob CC BY-NC-SA 4.0. A fronteira não é por diretório — ela atravessa
arquivos: num visualizador, o componente é código e as strings didáticas (notas de passo,
Python da tela, rótulos que explicam) são conteúdo. Regra completa e exemplos em
LICENSEeLICENSE-CONTENT. Contribuições entram sob a licença correspondente ao que a mudança toca.
- Next.js 16 (App Router) + React 19, export estático (
output: "export"→out/). Node 22+. - Conteúdo em MDX. Partes interativas são ilhas
"use client"; o resto é estático (SSG). - Deploy: Cloudflare Pages via Wrangler (ver seção Deploy e
.github/workflows/).
npm run dev # desenvolvimento
npm run build # DEVE passar (gera ./out). 55+ páginas.
npm test # Playwright (roda contra o ./out via python http.server). DEVE passar.
PORT=3101 npm test # porta alternativa: obrigatório quando há mais de uma suíte na máquina- Depois de mudar código, rode
npm run buildenpm test. Os dois têm que passar. A suíte não reusa servidor que já esteja na porta: reusar fazia ela testar oout/de outro worktree (ou o dev server, que serve da fonte) e passar verde com código quebrado. Porta ocupada agora falha dizendo isso; usePORT=<outra> npm test. - Para conferência visual, use o
agent-browser(Vercel Labs CLI, instalado na máquina):- Servir o build:
python3 -m http.server 4321 --directory oute abrir nele; ou usar o dev server. - Screenshot da página inteira: flag é
--full(não--full-page). - Mobile:
agent-browser set viewport 390 844(depois deopen). Confira overflow comdocument.body.scrollWidth > window.innerWidth. - Overlay/modal (position:fixed): use screenshot de viewport (sem
--full). find role buttonnão acha botões cujo nome começa com glifo (⤢). Clique viaagent-browser eval "document.querySelector('.classe').click()".
- Servir o build:
- Arquivos temporários (prints, GIFs, artefatos) vão para
/screenshots/(está no.gitignore). Nunca commite esses arquivos.
content/fundamentos.tsé a fonte única dos FUNDAMENTOS, a sequência principal (dirige o menu lateral, o/fundamentos/, as tags e o SEO).content/fica na raiz, irmão desrc/:src/é o código de estrutura,content/são os dados, artigos e visualizadores. Agrupamento estilo LeetCode (16 grupos): cada estrutura junto das técnicas que operam sobre ela; paradigmas (Recursão, Backtracking, Programação Dinâmica, Greedy Algorithms) como grupos próprios.content/roadmaps/é a casa dos roadmaps extras, um arquivo por roadmap (content/roadmaps/<slug>.ts), comindex.tsguardando o modelo, o registro e os derivados das três casas.content/avulsos.tsguarda os tópicos que se bastam numa página só. O índice é uma lista à mão porque o módulo é importado por componente de cliente (oShelldecide a barra lateral com ele) e código de cliente não temfs; quem impede que ela envelheça é o testetodo arquivo de content/roadmaps/ está registrado no índice, que lê a pasta.- Vocabulário, e ele importa: Fundamentos é a sequência principal; roadmap é um percurso extra; tópico é uma página. "Trilha" e "curso" não existem mais no produto nem no código (só sobrou "Trilha de navegação", que é o nome do breadcrumb).
- A página canônica de um tópico é sempre
/topicos/<slug>/, venha ele de onde vier. Quem precisa de todos os tópicos do site (generateStaticParams, sitemap, guarda de datas) usaSITE_TOPICS; quem fala dos Fundamentos (progresso, barra lateral, números da home) usaALL_TOPICS. Trocar um pelo outro não dá erro — dá número errado na home ou 404 num tópico. - UM TÓPICO PODE ESTAR EM MAIS DE UM ROADMAP. O grupo de um roadmap aceita um
Topicescrito ali (ele é o DONO) ou umastringcom o slug (ele só CITA). O dono decide a casca da página canônica e reivindica o slug; quem cita ganha o tópico na lista e uma URL própria (/roadmaps/<r>/<topico>/) comcanonicalde volta para/topicos/<slug>/e fora do sitemap. Exemplos vivos:content/roadmaps/caminhos-minimos.tscita 6 tópicos e não tem nenhum próprio (todos são dos Fundamentos, em outra ordem); a Skip List não é citada por roadmap nenhum e por isso abre sem barra lateral. O namespace é global e o build cobra: slug de tópico, id de grupo, slug de roadmap, citação que resolve e tópico não repetido são conferidos no import decontent/roadmaps/index.ts. Como escolher a casa de um tópico novo: ver a tabela em README. - Identificadores em inglês; qualquer coisa que o aluno lê em português. Vale para os
campos do roadmap e para o código dos visualizadores (variáveis, tipos, props, funções).
Comentários podem ser em português; nome de componente, o que fizer sentido. A fronteira que
já mordeu: o código Python que aparece na tela, os rótulos das variáveis e as notas do passo a
passo são conteúdo didático, mesmo dentro de uma string — renomear em lote traduz o
identificador e estraga a aula junto ("O array precisa estar sorted"). Detalhes e o
procedimento de conferência em
content/visualizers/README.md§0. - O tópico não tem casa. Cada um é uma pasta com dois arquivos:
content/topicos/<slug>/index.ts(o dado:topico, osumariodo artigo e apratica) econtent/topicos/<slug>/artigo.mdx(o texto). Um terceiro arquivo ali reprova no teste: quando o peso obriga a separar, quem separa é o REGISTRO, não a pasta. - Os roadmaps CITAM tópicos, em
content/roadmaps/<slug>.ts, portopics: [{ topic: "hash-table" }]. Um tópico pode ser citado por nenhum, por um ou por seis; o tópico não muda por isso e não sabe quem o cita. Os Fundamentos são um roadmap como os outros. - Três registros à mão, porque
content/topicos/index.tsé importado por componente de cliente (a barra lateral) e cliente não temfs:content/topicos/index.ts(os tópicos),content/topicos/artigos.ts(os.mdx) econtent/topicos/pratica.ts(problemas e referências). Os dois últimos só o servidor importa, e é isso que mantém 2,1 MB de artigo e 64 KB de problema fora de toda página. Esqueceu de registrar? O arquivo fica na pasta e o site não o serve — quem cobra étests/roadmaps.spec.ts, que lê o diretório e compara nos dois sentidos. - Os TIPOS moram em
src/content/tipos.ts, não emcontent/. Na raiz,content/é só conteúdo; o modelo é código. Um tópico novo importaTopicde@/content/tipos. - Campos do tópico:
name,group,level,description,youtube(id),article(link do blog),extraVideos,viz,noViz,status: "ready" | "soon". Ogroupé o assunto ("Arrays e Strings"), não um endereço — quem dá endereço é o roadmap que cita. Dois tópicos com o mesmo assunto têm de escrevê-lo igual: duas grafias viram duas seções em/topicos/com o mesmo título e o mesmoidde âncora (já aconteceu, e a lista passou a duplicar linhas). - "Em breve" é só para tópico vazio.
isEmptyTopic()(emcontent/topicos/index.ts) =soonsemyoutube,articlenemviz.extraVideosNÃO entra na conta, de propósito e com a razão escrita no código: são links para resoluções soltas de exercício, e um tópico que só tem isso continua sem aula, sem texto e sem visualização. (A doc já disse o contrário do código aqui; quem vale é o código.) Só os vazios levam o selo "em breve" no menu lateral e onoindex; quem já tem qualquer material aparece normal. Tópico que nunca vai ter visualizador recebenoViz: truee deixa de mostrar o aviso de "visualização em construção". - "NOVO" é tag manual, não data. O selo do menu lateral vem de
isNew: trueno tópico. Quem publica um tópico põe a tag no mesmo PR e tira a dos anteriores — sem data, nada envelhece sozinho. Não derive o selo de outro campo (já foiviz, e virava "novo" permanente). - Código no MDX: sempre com linguagem na cerca (
```python). O Shiki roda no build (plugin rehype emnext.config.ts) e o HTML já sai colorido: zero JS de highlight no cliente, SSG intacto. A cerca também alimenta o selo discreto de linguagem no canto do bloco, montado emmdx-components.tsx(mapaLINGUAGENS). Cerca sem linguagem fica sem cor e sem selo, o que é o certo para diagrama em ASCII e pseudo-fórmula. Linguagem nova precisa entrar em duas listas:langs(next.config.ts) eLINGUAGENS(mdx-components.tsx). - Visualizadores ficam em
content/visualizers/(ex.:SlidingWindowVisualizer,TwoPointersVisualizer), expostos emmdx-components.tsx. A lista de apoiadores fica emsrc/app/apoie/apoiadores.ts(página fixa, não é conteúdo de DSA). - Vai mexer na casca de um visualizador (altura, expandido, teclado, foco, rolagem)?
Leia
content/visualizers/README.mdantes. Ele é o contrato da casca adaptativa (.viz-fit): as três camadas (comprimir antes de esconder), a decisão por medição em vez de breakpoint, a API de CSS, o contrato de teclado e foco do painel expandido, e as armadilhas já medidas. A mecânica é um hook,src/lib/visualizer.tsx(useVisualizer+VizHeadereVizFooter): chame, espalhe as props e use os componentes prontos — não reescreva. Ele cobre o que TODO visualizador tem (caber na tela, painel, bloco que mostra e oculta, controles de reprodução) e nada do que cada um mostra. Uso de referência:BigOCounterVisualizer.tsx. Não edite o blocoviz-fitdoglobals.csspara acomodar um visualizador específico — ele é compartilhado por todos. - Alias:
@/*→src/*,@content/*→content/*. - Adicionar tópico/visualizador: ver README (seções "Como adicionar"). O padrão de visualizador é gerador puro de passos + casca compartilhada + botão Expandir.
src/lib/links.tsé ponto único. Comunidade =LINKS.site(craftcodeclub.io); repo =LINKS.github(craft-code-club/roadmap-dsa); apoio =LINKS.apoiar(campanha na APOIA.se).- Discord tem duas portas, por design. Dentro do app:
LINKS.discord, convite direto, um clique só. Fora do app (README, CONTRIBUTING, SECURITY, templates do.github/, qualquer.md):https://craftcodeclub.io/join, que é o ponto de rotação da comunidade. Nunca colediscord.gg/<código>cru em arquivo de documentação. - Barra: esquerda = Início, Fundamentos, Roadmaps, Tópicos; direita = YouTube, Discord,
Apoiar + menu
⋯. O menu⋯tem: Sobre o projeto, Craft & Code Club, GitHub do projeto, Apoiadores e Parceiros — e, conforme a largura aperta, os itens que saíram da barra. - A barra encolhe em degraus, e a ordem é comprimir antes de esconder (a mesma filosofia da
casca dos visualizadores). São quatro, todos no
globals.css: 1000px comprime o respiro (é onde nasce a gaveta e ela rouba 42px); 940px manda Início e YouTube para o⋯(nav-hide-mdna barra,only-mobile only-mdno menu); 760px manda Fundamentos, Roadmaps e Tópicos (nav-hide-sm/only-mobile); 400px comprime o que sobrou. Duas regras que não se negociam: nada some da barra sem aparecer no⋯, e as peças da barra são rígidas (semmin-width: 0, sem quebra de linha). A rigidez é de propósito: peça que encolhe em silêncio se sobrepõe sem estourar a página, e nenhuma medição de overflow pega isso — foi assim que a marca passou a quebrar em três linhas em 920px. Quem mede os quatro degraus étests/barra-do-topo-responsiva.spec.ts, varrendo 1400 → 320px. - Um tópico tem DUAS URLs, e elas não competem.
/topicos/<slug>/é a canônica: quem chega pelo índice geral, sem percurso, e vê na barra os roadmaps que citam o tópico./roadmaps/<r>/<slug>/é a mesma página dentro de um percurso: menu do roadmap, anterior/próximo, e as citações do artigo reescritas para o roadmap (linkDentroDoRoadmap), para o leitor não cair fora dele ao clicar numa referência. Toda cópia apontacanonicalpara a canônica e fica fora do sitemap. - A casca muda com a rota, e é o
Shellque decide (layoutDaRota): a barra do roadmap em/roadmaps/<slug>/e em tudo abaixo dele, a barra dos roadmaps-que-citam em/topicos/<slug>/, e nenhuma barra na vitrine/roadmaps/, no índice/topicos/e nos tópicos que ninguém cita. A ausência é a decisão, não um esquecimento: num tópico avulso a barra ao lado seria uma lista para lugar nenhum.⚠️ OlayoutDaRotacasa o PRIMEIRO SEGMENTO da rota por string. Renomeou rota? Aqueleifnão quebra o build nem o tipo: ele silenciosamente devolve a casca padrão, e o roadmap inteiro passa a abrir com a barra dos Fundamentos. Mexeu noShellou numa rota? Rodetests/roadmaps.spec.ts, que é quem mede as cascas — foi ele que pegou exatamente isso. - Rota renomeada leva 301 no mesmo PR, em
public/_redirects: hoje/topico/*→/topicos/*,/fundamentos/*e/roadmap*→/roadmaps/fundamentos/*. E a rota de origem não pode existir como arquivo noout/, senão o Pages serve o arquivo e a regra nunca roda. - Links externos mostram
↗(classeext; a regra CSS é.topnav > a.extpara não afetar o menu). Bolinhas de marca: Discord blurple, YouTube vermelho, Apoiar âmbar.
sitemap.ts,robots.tseopengraph-image.tsxexistem. Rotas de metadata precisam deexport const dynamic = "force-static"por causa dooutput: "export".- Tópicos realmente vazios (
soonsem youtube, article nem viz) recebemnoindex(vergenerateMetadataemtopico/[slug]/page.tsx). Ao ganhar conteúdo, saem do noindex sozinhos. - A mesma régua vale um nível acima: a abertura de um roadmap sem NENHUM tópico com material
também é
noindexe fica fora do sitemap (roadmapHasMaterial). Ela continua no site (mapear o território vale para quem estuda), mas não pede lugar no índice. /roadmaps/<r>/<topico>/serve o mesmo texto de/topicos/<slug>/, então ela apontacanonical(eog:url) para a canônica, fica FORA do sitemap e não emite JSON-LD nenhum: quem declara o recurso é a canônica, e declarar de novo numa página que acabou de dizer "a principal é outra" é a mesma contradição donoindexcomLearningResource.- O rastro de navegação tem quatro formas (Fundamentos, roadmap, avulso, e o tópico dentro de
um roadmap) e a página monta o array de migalhas UMA vez, usando-o nos links e no
BreadcrumbList. Não recrie a decisão dentro dojsonld.ts: o teste "o rastro marcado é o rastro desenhado" existe por causa disso.
- Nos grids que colapsam no mobile, use
minmax(0, 1fr)(não1frpuro, que vira min-content e estoura a largura) emin-width: 0nos itens que precisam encolher (article,.mdx-cartao,.prose-pre). Já verificado: 0 overflow em 360/390/768px.
Site estático (out/) publicado no Cloudflare Pages via Wrangler. O deploy roda no CI
(.github/workflows/cloudflare-pages-deploy.yml) em push para main; PRs de forks não fazem
deploy, e todo PR precisa da aprovação de um mantenedor antes do merge. Deploy local:
npx wrangler pages deploy out --project-name <nome>. Não há wrangler.toml: o nome do projeto
vem de --project-name (no CI, do secret). Secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PROJECT_NAME (e, para apoiadores, APOIASE_KEY / APOIASE_SECRET /
APOIASE_CAMPAIGN).
Credencial da APOIA.se: três nomes, e nenhum é intercambiável. APOIASE_KEY vai no header
x-api-key, APOIASE_SECRET no Authorization: Bearer (é um JWT) e APOIASE_CAMPAIGN é o id da
campanha. Sem os dois primeiros, o muro de /apoie sai da lista de plano B de
src/app/apoie/apoiadores.ts e o build avisa no log — o return calado dessa mesma linha é o
que fez a página publicada mostrar 3 apoiadores com 5 apoios na campanha, por meses, sem nada
denunciar. A varredura que prova que credencial nenhuma chega ao out/ é
tests/segredo-nao-vai-para-o-cliente.spec.ts; ela lê os 1.242 arquivos do build, HTML, chunks e
payload RSC, e entra com 0 ocorrência em 16 regras.
- Google Analytics 4 é opt-in por ambiente.
src/components/Analytics.tsxsó renderiza oGoogleAnalyticsdo@next/third-partiesquandoNEXT_PUBLIC_GA_IDexiste; sem a variável ele devolvenulle nenhum byte do gtag.js é pedido. O workflow injeta o ID só no build damain(é uma variable,vars.NEXT_PUBLIC_GA_ID, não um secret: o ID sai no HTML de toda página). Preview de PR, build de fork enpm run devficam sem analytics — de propósito, para não sujar a propriedade nem medir o desenvolvedor. - Não troque o componente do
@next/third-partiespor um<script>na mão: o snippet do Google é síncrono e bloqueia o parser; o componente usanext/scriptcomafterInteractivee cuida da deduplicação entre navegações client-side do App Router. - Google Search Console: a verificação é por DNS, propriedade de domínio, com TXT no
Cloudflare — não depende de deploy nem de arquivo no
out/.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION(que virametadata.verification.googleno layout) é o plano B para quem só tem propriedade de prefixo de URL. As duas variáveis estão documentadas no.env.example.
- Conventional Commits (
feat,fix,docs,ci,chore, ...). Commits atômicos. Ver CONTRIBUTING para o padrão completo. - Não commite
node_modules,out,.next, prints (/screenshots/) — o.gitignorecobre. - CI:
tests.yml(Playwright),cloudflare-pages-deploy.yml(deploy fork-guarded; secretsCLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_PROJECT_NAME),commitlint.yml(Conventional Commits nos PRs),codeql-analysis.yml(só roda quando o repositório for público).