Visão Geral
Este projeto implementa uma plataforma de documentação interna (in-house) seguindo os princípios de "Documentation as Code". O sistema permite produzir, revisar e publicar documentação técnica usando ferramentas familiares para desenvolvedores, incluindo controle de versão, pull requests e ambientes de preview.
Motivação
A necessidade de manter documentação atualizada e acessível levou à criação de um sistema que:
- Usa Markdown como formato principal para simplificar a contribuição
- Oferece um fluxo de revisão baseado em Pull Requests
- Automatiza a publicação via GitHub Actions
- Disponibiliza ambientes de preview para revisão das alterações
Linha do Tempo de Desenvolvimento
- Fase 1: Estrutura básica do repositório e configuração do GitHub Pages
- Fase 2: Desenvolvimento dos componentes front-end (HTML, CSS, JavaScript)
- Fase 3: Implementação dos scripts auxiliares para processar arquivos Markdown
- Fase 4: Configuração do workflow de CI/CD para publicação automática
- Fase 5: Implementação do sistema de preview para Pull Requests
Arquitetura do Sistema
A arquitetura do sistema é intencionalmente simples, baseada em arquivos estáticos e processamento no lado do cliente.
Estrutura de Diretórios
docs-as-code/
├── .github/
│ └── workflows/
│ └── documentation-workflow.yml # Workflow CI/CD unificado
├── css/
│ └── styles.css # Estilos do site
├── js/
│ ├── main.js # Lógica principal da interface
│ └── markdown-processor.js # Processamento de Markdown
├── scripts/
│ ├── cria_menu.js # Geração da estrutura de menu
│ └── process_metadata.js # Processamento de metadados
├── index.html # Página principal
└── [documentos markdown] # Documentação em formato .md
Componentes Front-end
HTML (index.html)
O arquivo index.html serve como ponto de entrada da aplicação, com uma estrutura de single-page application:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documentação Técnicatitle>
<link rel="stylesheet" href="css/styles.css">
head>
<body>
<header>
<div id="companyLogo">Logodiv>
<div class="version-selector">
<!-- Seletor de versão e idioma -->
div>
header>
<div class="container">
<nav id="sidebar">
<!-- Menu lateral gerado dinamicamente -->
nav>
<main id="content">
<!-- Conteúdo carregado dinamicamente -->
main>
<aside class="toc-container">
<!-- Tabela de conteúdos gerada automaticamente -->
aside>
div>
<script src="js/marked.min.js">script>
<script src="js/main.js">script>
body>
html>
CSS (styles.css)
O CSS foi desenvolvido com foco em legibilidade e responsividade, usando um sistema de cores baseado em tons de verde para identidade visual:
:root {
--primary-color: #00766C; /* Verde turquesa médio */
--primary-light: #00B29A; /* Verde turquesa */
--primary-dark: #004D40; /* Verde escuro */
--accent-color: #00E1C4; /* Verde turquesa brilhante */
--background-light: #f0faf8; /* Verde bem claro */
--text-color: #333333;
--text-light: #666666;
}
/* Layout responsivo */
.container {
display: flex;
height: calc(100vh - 70px);
}
/* Cabeçalho */
header {
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2rem;
background-color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* Sidebar de navegação */
#sidebar {
width: 300px;
height: 100%;
overflow-y: auto;
padding: 1.5rem;
background-color: var(--background-light);
border-right: 1px solid var(--primary-light);
}
/* Conteúdo principal */
#content {
flex: 1;
height: 100%;
overflow-y: auto;
padding: 2.5rem;
background: white;
}
/* Tabela de conteúdos */
.toc-container {
width: 300px;
height: 100%;
overflow-y: auto;
padding: 1.5rem;
border-left: 1px solid var(--primary-light);
background: var(--background-light);
}
/* Componentes de callout */
.callout {
margin: 1.5rem 0;
padding: 1.25rem;
border-radius: 8px;
border-left: 4px solid;
position: relative;
}
/* Responsividade */
@media (max-width: 1200px) {
.toc-container {
display: none;
}
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
#sidebar {
width: 100%;
height: auto;
max-height: 300px;
}
}
JavaScript (main.js)
O arquivo principal de JavaScript gerencia o carregamento de conteúdo, interações do usuário e processamento de Markdown:
document.addEventListener('DOMContentLoaded', function() {
// Estado da aplicação
let currentPath = '';
let currentVersion = 'v4.0'; // Versão padrão
let currentLanguage = 'pt-BR'; // Idioma padrão
// Inicialização
loadSidebar();
// Carrega menu lateral com estrutura da documentação
function loadSidebar() {
fetch('structure.json')
.then(response => response.json())
.then(data => {
renderSidebar(data);
// Verifica URL para carregar conteúdo específico
const urlParams = new URLSearchParams(window.location.search);
const articlePath = urlParams.get('article');
const homePage = urlParams.get('home');
if (articlePath) {
loadContent(articlePath);
} else if (homePage) {
loadIndexContent();
} else {
// Tenta restaurar o último artigo visualizado
const savedPath = localStorage.getItem('currentPath');
if (savedPath) {
loadContent(savedPath);
} else {
loadIndexContent();
}
}
})
.catch(error => {
console.error('Error loading sidebar:', error);
loadFallbackContent();
});
}
// Renderiza conteúdo Markdown
function loadContent(path) {
currentPath = path;
// Salva o caminho atual no localStorage
localStorage.setItem('currentPath', currentPath);
fetch(path)
.then(response => response.text())
.then(markdown => {
// Pré-processar o Markdown para lidar com os callouts
const processedMarkdown = preProcessCallouts(markdown);
// Renderizar com o Marked
const htmlContent = marked.parse(processedMarkdown);
document.getElementById('content').innerHTML = `
${htmlContent}
`;
// Gerar tabela de conteúdos
generateTOC();
// Ativar scroll spy
setupScrollSpy();
})
.catch(error => {
console.error('Error loading content:', error);
loadFallbackContent();
});
}
// Função para pré-processar callouts no Markdown
function preProcessCallouts(markdown) {
// Expressão regular para encontrar callouts
const calloutRegex = /:::\((\w+)\)\s+\(([^\n]+)\)([\s\S]*?)(?=\n:::)/gm;
let processedMarkdown = markdown.replace(calloutRegex, function(match, type, title, content) {
return `${type.toLowerCase()}">
${title}
${content}
`;
});
// Remove as tags de fechamento de callout
processedMarkdown = processedMarkdown.replace(/\n:::/g, '');
return processedMarkdown;
}
// Gera tabela de conteúdos a partir dos headings
function generateTOC() {
const headings = document.querySelectorAll('#content h1, #content h2, #content h3');
const toc = document.querySelector('.toc-container');
if (!toc || headings.length === 0) return;
let tocHTML = 'Conteúdo';
headings.forEach(heading => {
const level = heading.tagName.substring(1);
const text = heading.textContent;
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
heading.id = id;
tocHTML += <a href="#${id}" class="toc-item h${level}">${text}a>;
});
tocHTML += '';
toc.innerHTML = tocHTML;
}
});
Scripts Auxiliares
Geração de Menu (cria_menu.js)
Este script Node.js escaneia o diretório de documentação para gerar um arquivo JSON com a estrutura de navegação:
const fs = require('fs');
const path = require('path');
const { marked } = require('marked');
const docsRoot = './';
const outputFile = './structure.json';
// Lista de arquivos e diretórios a serem ignorados
const ignoredItems = [
'structure.json',
'index.html',
'styles.css',
'script.js',
'.git',
'api-specs',
'node_modules',
'.github',
'.vscode',
'cria_menu.js'
];
// Função para extrair o título H1 do conteúdo Markdown
function extractTitle(markdownContent) {
const tokens = marked.lexer(markdownContent);
const h1 = tokens.find(token => token.type === 'heading' && token.depth === 1);
return h1 ? h1.text : null;
}
// Função principal para escanear diretórios
function scanDirectory(dir) {
const items = [];
const files = fs.readdirSync(dir);
files.forEach(file => {
if (ignoredItems.includes(file)) return;
const fullPath = path.join(dir, file);
const relativePath = fullPath.replace(docsRoot, '');
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
// Processar diretório
const children = scanDirectory(fullPath);
items.push({
type: 'directory',
name: file,
path: relativePath,
children: children
});
} else if (path.extname(file) === '.md') {
// Processar arquivo Markdown
try {
const content = fs.readFileSync(fullPath, 'utf8');
const title = extractTitle(content) || file.replace('.md', '');
items.push({
type: 'file',
name: title,
path: relativePath,
originalName: file
});
} catch (error) {
console.error(Erro ao processar ${fullPath}:, error);
}
}
});
return items;
}
try {
console.log('Gerando estrutura da documentação...');
const structure = scanDirectory(docsRoot);
fs.writeFileSync(outputFile, JSON.stringify(structure, null, 2));
console.log('Estrutura gerada com sucesso!');
console.log(Total de ${countItems(structure)} itens processados.);
} catch (error) {
console.error('Erro ao gerar estrutura:', error);
}
// Função auxiliar para contar itens processados
function countItems(items) {
let count = items.length;
items.forEach(item => {
if (item.type === 'directory' && item.children) {
count += countItems(item.children);
}
});
return count;
}
Processamento de Metadados (process_metadata.js)
Este script processa metadados nos arquivos Markdown, convertendo blocos de metadados para títulos H1:
const fs = require('fs');
const path = require('path');
// Caminho para o diretório raiz
const rootDirectory = path.join(__dirname);
// Função para percorrer o diretório recursivamente
function traverseDirectory(dir, callback) {
fs.readdir(dir, (err, files) => {
if (err) {
console.error(Erro ao ler o diretório ${dir}:, err);
return;
}
files.forEach(file => {
const filePath = path.join(dir, file);
fs.stat(filePath, (err, stats) => {
if (err) {
console.error(Erro ao obter informações do arquivo ${filePath}:, err);
return;
}
if (stats.isDirectory()) {
// Se for um diretório, chama a função recursivamente
traverseDirectory(filePath, callback);