Este documento explica o pipeline que publica a página que você está lendo: onde o conteúdo vive, como chega ao banco, como é servido e buscado, e como o site estático de docs se reconstrói quando o conteúdo muda.
flowchart TD
subgraph write["1 · Escrever"]
GH["📦 Repositório beyou-arch-design<br/>markdown + YAML + mermaid"]
end
subgraph automate["2 · Automatizar (no push para main)"]
WF["⚙️ GitHub Action<br/>login → importação × 4 → dispatch"]
end
subgraph store["3 · Importar e guardar"]
BE["🍃 Serviços de importação do backend"]
DB[("🐘 Tabelas docs_*")]
end
subgraph serve["4 · Servir e renderizar"]
API["API pública /docs"]
UI["⚛️ SPA docs-ui"]
PRE["🖼️ Site estático pré-renderizado<br/>reconstruído como imagem nova"]
end
GH --> WF
WF -->|"JWT Bearer + segredo de importação"| BE
BE -->|"API de conteúdo do GitHub"| GH
BE --> DB
DB --> API
API --> UI
WF -->|"repository_dispatch"| PRE
PRE -->|"GHCR → Watchtower"| LIVE["docs.beyouweb.com"]
Um merge de conteúdo na main é o deploy inteiro: o workflow importa cada área e, quando as quatro passam, dispara o build do docs-ui, que pré-renderiza cada rota contra a API fresca e publica uma imagem nova que o Watchtower pega. Mudança de conteúdo chega como container novo, nunca como restart.
Toda área segue a mesma forma: um diretório por tópico, um descritor YAML e um arquivo markdown por idioma (en.md, pt.md; o idioma é o nome do arquivo).
| architecture | blog | api | projects | |
|---|---|---|---|---|
| Descritor | topic.yaml | topic.yaml | controller.yaml | topic.yaml |
| Também obrigatório | diagram.mmd | nada mais | openapi.yaml | diagram.mmd |
| Pelo menos um .md | sim | sim | sim | sim |
| Valores de status | ACTIVE, DRAFT, ARCHIVED | ACTIVE, ARCHIVED | ACTIVE, DRAFT, ARCHIVED | ACTIVE, DRAFT, ARCHIVED, PLANNING |
O descritor do blog é o rico: categoria (TECHNICAL ou PLANNING), tags, flag de destaque, publishedAt, autor, emoji de capa e uma cor de capa que precisa casar com um hex de seis dígitos ou é descartada em silêncio. O frontmatter do markdown é um parser plano feito à mão, não YAML completo: só title e summary são lidos, chaves desconhecidas são ignoradas e uma cerca de fechamento faltando falha a importação. Um arquivo sem frontmatter é legal; o título cai para a chave do tópico.
O armazenamento segue um padrão repetido: uma linha de tópico (identidade, ordem, status) mais uma linha de conteúdo por idioma (título, resumo, docMarkdown e, por área, o diagrama mermaid ou o texto cru do OpenAPI). Duas curiosidades que valem saber: o diagrama é copiado em cada linha de idioma, e a área de projects guarda seus campos relacionais (URL do repositório, chaves de tópicos ligados) na linha de conteúdo, não no tópico.
O POST /docs/admin/import/{area} fica atrás de duas portas independentes: a requisição precisa de um JWT válido (qualquer usuário autenticado) e o header X-Docs-Import-Secret precisa casar com o segredo configurado em tempo constante. O comentário do workflow memoriza o diagnóstico: 401 significa que o token foi rejeitado, 403 que o segredo foi.
O importador caminha pela API de conteúdo do GitHub: lista a raiz da área, lista cada diretório de tópico, busca cada arquivo, decodifica o base64. Dono, nome, branch e caminho do repositório vêm da configuração e podem ser sobrescritos por requisição. Depois, por tópico:
Cada área importa em uma transação, com seus caches evictados ao final, então leitores nunca veem meia importação. As quatro áreas são quatro chamadas HTTP separadas, porém: uma falha no meio deixa as áreas anteriores commitadas e pula a reconstrução do site. E o modo de falha é rígido de propósito: um diretório de tópico malformado aborta a área inteira. Um exemplo real: um diretório de controller já chegou só com o openapi.yaml, e toda importação falhou com Missing controller.yaml in api/xp até os arquivos de metadados chegarem.
Uma borda operacional afiada: sem um token do GitHub configurado, o importador roda no limite anônimo de 60 requisições por hora, e uma rodada completa das quatro áreas precisa de cerca de 175 chamadas. A falha aparece como um genérico "could not fetch". Na prática, o token não é opcional.
O workflow publish-docs dispara em qualquer push na main que toque os quatro diretórios de conteúdo. Três passos, todos curl:
X-Access-Token, mascarado nos logs.docs-content-updated para o repositório do docs-ui (com um PAT, já que o token padrão do workflow não levanta eventos entre repositórios). O CI do docs-ui então pré-renderiza cada rota por idioma contra a API pública e publica a imagem nova.Execuções entram em fila em vez de serem canceladas sob concorrência, por uma razão sutil: o passo de arquivamento de uma execução velha terminando depois de uma nova poderia arquivar tópicos que a nova acabou de importar.
Todos os endpoints de leitura são públicos e cientes de idioma (?locale=, padrão inglês, caindo para inglês quando um idioma falta). Listas devolvem só tópicos ACTIVE ordenados por orderIndex (o blog ordena por data de publicação); detalhes buscam por chave.
| Área | Lista | O detalhe adiciona |
|---|---|---|
| /docs/architecture/topics | chave, título, resumo, ordem, status, tags, projectKey | diagramMermaid, docMarkdown |
| /docs/blog/topics | campos de capa, categoria, tags, destaque, autor, datas | docMarkdown |
| /docs/api/controllers | chave, título, resumo, ordem | apiCatalog (o OpenAPI cru) |
| /docs/projects/topics | chave, título, resumo, ordem, status, tags | docMarkdown, diagrama, repositoryUrl, chaves ligadas |
As tags cruzam o fio como uma string JSON, não um array; os clientes fazem o parse. Uma nota de honestidade: só as consultas de lista filtram por status. Um tópico ARCHIVED ou DRAFT continua alcançável pela URL direta, então arquivar tira da lista, não do ar.
O GET /docs/search é híbrido: uma consulta SQL por área afunila candidatos por casamento sem caixa em título e resumo, e então pontuação, destaque, ordenação e paginação acontecem em memória sobre o conjunto unido. Acerto no título pontua 1.0, no resumo 0.5. Os destaques voltam como fragmentos alternados de texto puro e marcado, que o cliente concatena. O corpo dos documentos nunca é buscado, só títulos e resumos.
Leituras são cacheadas (dois caches Caffeine por área, TTL de 120 minutos, derrubados por inteiro a cada importação); a busca não é.
O docs UI busca esses endpoints e renderiza markdown com suporte a GFM, blocos mermaid inline como diagramas vivos, o diagrama principal do tópico em um painel expansível e um trilho de sumário na borda direita. As cores do mermaid são reconstruídas do tema ativo a cada troca. Páginas de detalhe são endereçadas por caminho (/architecture/{chave} sob um prefixo de idioma), e a listagem de arquitetura abre seu primeiro tópico automaticamente. Para os crawlers, cada rota também existe como HTML estático pré-renderizado dentro da imagem do site, construído no build, que é o que mantém o conteúdo legível para buscadores e crawlers de IA que nunca rodam JavaScript.
| Área | Problema |
|---|---|
| Cross-link de projects | O parser lê um campo designTopicKey enquanto todos os arquivos de conteúdo declaram blogTopicKey, então o link cruzado para o blog nunca chega à UI. Parser e conteúdo precisam concordar |
| Locale da busca | O endpoint de busca pula a normalização de idioma que os endpoints de leitura usam, então um locale maiúsculo ou regional devolve nada |
| Cliente do GitHub | O RestTemplate da importação não tem timeouts; uma resposta travada do GitHub segura a transação aberta |
| Duplicação | Quatro serviços de importação quase idênticos com helpers copiados e três cópias do parser de frontmatter |
| Cobertura de testes | Testes de importação existem só para a área de arquitetura; blog, api e projects estão sem testes |