# Documentação técnica — Marketing Para Comércios

Este documento descreve a arquitetura do site, o funcionamento do gerador estático, o ciclo de vida do service worker, o fluxo da busca interna e as convenções de código HTML, CSS e JavaScript adotadas no projeto.

Para o manual editorial e o processo de publicação, veja [`README.md`](README.md). Para a estratégia de busca, [`seo.md`](seo.md).

---

## 1. Princípios de arquitetura

### 1.1 HTML-first

Todo conteúdo visível existe fisicamente no arquivo `.html` entregue pelo servidor. Isso inclui menus, listagens, cards, índices de artigo, datas, artigos relacionados, trilhas de navegação e rodapé.

Consequências práticas:

- O site é integralmente legível, navegável e indexável com JavaScript desativado.
- Não existe estado de carregamento, esqueleto de conteúdo ou requisição de dados para renderizar texto.
- O tempo até o conteúdo aparecer é o tempo de download do HTML.

### 1.2 Zero dependências em produção

Não há framework, biblioteca de CSS, jQuery, empacotador nem runtime. A página carrega três recursos próprios: uma folha de estilo, um script de melhorias progressivas e — apenas em `/busca/` — o script da busca.

### 1.3 Geração em tempo de autoria

O gerador (`tools/build.mjs`) roda na máquina do editor e grava HTML no repositório. O servidor não executa nada. Isso significa que qualquer hospedagem de arquivos estáticos serve o projeto, e que o resultado do build é auditável no controle de versão.

### 1.4 Progressive enhancement

O JavaScript adiciona conveniências, nunca funcionalidade essencial:

| Recurso | Sem JavaScript |
|---|---|
| Menu principal | Visível e navegável (só colapsa em telas pequenas quando o JS está ativo) |
| Barra de progresso | Ausente; a leitura não muda |
| Copiar link | Botão permanece oculto; há o link direto em HTML |
| Compartilhamento nativo | Botão oculto; os links de WhatsApp, LinkedIn, Facebook, Pinterest e e-mail são âncoras HTML |
| Busca interna | Mensagem em `<noscript>` com caminho alternativo por categorias e sitemap |
| PWA e offline | Não registrados; o site funciona normalmente online |

---

## 2. Estrutura de diretórios

```
raiz do repositório
├── *.html, *.txt, *.xml, *.json      arquivos servidos
├── artigos/[slug]/index.html         50 artigos
├── categorias/[slug]/index.html      10 categorias + índice
├── [pagina]/index.html               9 páginas institucionais
├── assets/{css,js,images,icons,fonts}
├── data/{content-manifest,search-index}.json
│
├── content/articles/*.md             fonte dos artigos
├── content/pages/*.md                fonte das páginas institucionais
└── tools/
    ├── site.config.mjs               configuração central
    ├── build.mjs                     gerador
    ├── check.mjs                     auditoria
    ├── wordcount.mjs                 contagem de palavras
    └── lib/
        ├── markdown.mjs              parser de Markdown + front matter
        ├── templates.mjs             head, header, footer, layout, JSON-LD
        └── images.mjs                geração de SVG e codificação de PNG
```

---

## 3. O gerador estático

### 3.1 Fluxo de execução

`node tools/build.mjs` executa, em ordem:

1. **`loadArticles()`** — lê `content/articles/*.md`, separa front matter do corpo, converte Markdown em HTML, extrai a árvore de headings, conta as palavras úteis e calcula o tempo de leitura. Ordena por data.
2. **`validate()`** — verifica as invariantes do projeto. Em caso de falha, imprime a lista de problemas e encerra com código 1 (a menos que se passe `--force`).
3. **`computeRelated()`** — resolve os 6 artigos relacionados de cada texto.
4. **`loadPages()`** — lê as páginas institucionais.
5. **Renderização** — artigos, categorias, índice de categorias, home, busca, 404 e institucionais.
6. **`buildAssets()`** — imagens destacadas em SVG, imagens Open Graph em PNG, ícones do PWA e favicon.
7. **`buildData()`** — `content-manifest.json` e `search-index.json`.
8. **`buildSitemap()`, `buildFeed()`, `buildRootFiles()`** — sitemap, RSS, robots, ads.txt, llms.txt, manifest e service worker.
9. **`buildContentPlan()`** — `content-plan.md`.

### 3.2 Invariantes verificadas no build

- Exatamente 50 artigos
- Datas formando a sequência de 2026-07-07 a 2026-08-25, sem repetição nem omissão
- Slugs únicos
- Mínimo de 1.300 palavras úteis por artigo
- Meta description com até 165 caracteres
- Pelo menos 4 títulos H2 por artigo
- `image_alt` presente
- Categoria existente em `site.config.mjs`
- Exatamente 6 relacionados por artigo (falha em tempo de execução se não conseguir compor)

### 3.3 Contagem de palavras úteis

`countWords()` opera sobre o HTML já renderizado do corpo do artigo: remove tags e entidades e conta tokens que contenham letra ou dígito. Isso exclui automaticamente cabeçalho do site, rodapé, índice, cards e metadados, que não fazem parte do corpo.

### 3.4 Tempo de leitura

`Math.max(1, Math.round(palavras / 200))`, com 200 palavras por minuto como referência para leitura em português. O valor aparece no HTML, no JSON-LD (`timeRequired`) e no manifesto.

### 3.5 Artigos relacionados

`computeRelated()` compõe a lista de 6 nesta ordem de prioridade:

1. Slugs declarados manualmente em `related:` no front matter
2. Artigos do mesmo cluster, percorridos circularmente a partir da posição do artigo atual, até completar 4
3. Um artigo de cada cluster afim, definido no mapa `affinity` de `site.config.mjs`
4. Complemento com os demais artigos dos clusters afins, se necessário

O resultado é determinístico: o mesmo conjunto de artigos sempre gera as mesmas relações, o que mantém o HTML estável entre builds.

### 3.6 Geração de imagens

**SVG (imagens destacadas).** `articleSvg()` produz um gradiente com a cor da categoria e formas geométricas posicionadas por um hash do slug. Cada artigo recebe uma composição própria, sem texto embutido — o que evita repetição visual e mantém o arquivo abaixo de 1 KB.

**PNG (Open Graph e ícones).** `encodePng()` implementa um codificador PNG mínimo com `zlib.deflateSync` e CRC-32 próprio: gera o `IHDR`, um `IDAT` com filtro `none` por linha e o `IEND`. É usado para as imagens Open Graph por categoria (1200×630) e para os ícones do PWA (192, 512, 512 maskable e 180 para Apple).

A escolha do formato é deliberada: SVG dentro da página, por ser nítido em qualquer densidade de tela e muito leve; PNG para Open Graph, porque os rastreadores de redes sociais geralmente não processam SVG.

### 3.7 Parser de Markdown

`tools/lib/markdown.mjs` implementa um subconjunto suficiente para o padrão editorial: headings H2–H4 com `id` gerado e desduplicado, listas ordenadas e não ordenadas com continuação em linhas indentadas, tabelas com cabeçalho, citações, blocos de fórmula com crase tripla, blocos de destaque `::: tipo Título … :::`, negrito, itálico, código curto e links (com `rel="noopener"` automático em links externos).

Todo texto passa por `escapeHtml()` antes da formatação inline, o que impede injeção de HTML a partir do conteúdo.

### 3.8 Versão de cache

A versão do service worker é o prefixo de 10 caracteres de um SHA-1 sobre a concatenação de slug, data de modificação e contagem de palavras de todos os artigos. Qualquer mudança de conteúdo produz uma versão nova, que invalida os caches antigos na ativação.

---

## 4. Service worker

Arquivo: `/sw.js`, gerado por `buildRootFiles()`.

### 4.1 Estratégias

| Tipo de requisição | Estratégia | Motivo |
|---|---|---|
| Navegação (`request.mode === 'navigate'`) | *network-first*, com fallback para cache e depois para `/offline/` | Garante que o visitante receba sempre a versão publicada mais recente enquanto houver rede |
| Demais recursos de mesma origem | *stale-while-revalidate* | Resposta imediata do cache com atualização silenciosa em segundo plano |
| Recursos de outras origens | Não interceptados | Evita cache de terceiros e problemas de opacidade |
| Métodos diferentes de `GET` | Não interceptados | — |

### 4.2 Ciclo de vida

**`install`** — abre o cache de assets, pré-carrega o *shell* mínimo (`/`, `/offline/`, `/categorias/`, `/busca/`, CSS, os dois scripts, favicon e manifesto) e chama `skipWaiting()`, para que a versão nova assuma sem esperar o fechamento de todas as abas.

**`activate`** — apaga todos os caches cujo nome não corresponda à versão atual e chama `clients.claim()`, assumindo o controle das páginas já abertas.

**`message`** — aceita a mensagem `skip-waiting`, para forçar a troca de versão a partir da página.

**`fetch`** — aplica as estratégias da tabela acima.

### 4.3 Por que o usuário nunca fica preso a uma versão antiga

Quatro mecanismos combinados:

1. Navegações são *network-first*: com rede, o HTML vem do servidor.
2. `registration.update()` é chamado em cada carregamento de página, em `main.js`.
3. `skipWaiting()` no install elimina a espera pela versão anterior.
4. O evento `controllerchange` recarrega a página uma única vez quando um service worker novo assume, com uma trava (`refreshing`) que impede laço de recarga.

### 4.4 O que o service worker nunca faz

- Não monta páginas nem injeta conteúdo editorial
- Não sintetiza respostas HTML
- Não intercepta requisições de outras origens
- Não guarda respostas com status diferente de 200

### 4.5 Depuração

```
Chrome DevTools › Application › Service Workers
  - "Update on reload" durante o desenvolvimento
  - "Unregister" + limpar Cache Storage para simular primeira visita
```

Como o registro exige `https:` (a verificação está em `main.js`), o service worker não interfere em testes locais por `file://` ou `http://`.

---

## 5. Busca interna

### 5.1 Por que é a única exceção à regra HTML-first

Uma busca estática exigiria uma página pré-renderizada por consulta possível, o que é inviável. A solução adotada mantém o restante do princípio intacto: a página `/busca/` é HTML completo — formulário, atalhos, índice de categorias e mensagem `<noscript>` —, e apenas a lista de resultados é montada no cliente.

### 5.2 Fluxo

1. O formulário usa `<form action="/busca/" method="get">` com `<input type="search" name="q">`. Sem JavaScript, o envio recarrega a página com `?q=termo` na URL — comportamento nativo do navegador.
2. `search.js` lê `q` com `URLSearchParams`, preenche o campo e atualiza o `<title>`.
3. Se `q` estiver vazio, exibe a instrução inicial e encerra.
4. Busca `/data/search-index.json` com `fetch`.
5. Normaliza a consulta (remove acentos, converte para minúsculas) e a divide em termos de mais de um caractere.
6. Pontua cada item: título +10 por termo, palavras-chave +6, categoria +4, descrição +3, e +15 se o título contiver a consulta inteira.
7. Ordena por pontuação, corta em 30 resultados e monta a lista.

### 5.3 Segurança da renderização

Nenhum uso de `innerHTML`, `insertAdjacentHTML`, `document.write` ou `eval`. Os nós são criados com `document.createElement()` e o texto é atribuído por `textContent`, que escapa qualquer conteúdo por construção. O parâmetro `q` é usado apenas como texto (em `textContent` e no `<title>`), nunca interpolado em markup.

O `href` de cada resultado vem exclusivamente do índice gerado no build — nunca da entrada do usuário.

### 5.4 Estados da interface

| Situação | Comportamento |
|---|---|
| Sem `q` | Instrução para digitar um termo |
| Buscando | "Procurando por …" via `role="status"` |
| Com resultados | Contagem e lista |
| Sem resultados | Mensagem com sugestões de termos mais curtos |
| Falha de rede | Mensagem com orientação para navegar por categorias |
| Sem JavaScript | `<noscript>` com caminho alternativo |

O elemento de status usa `role="status"` e `aria-live="polite"`, para que leitores de tela anunciem a mudança.

### 5.5 Formato do índice

```json
{
  "generatedAt": "2026-08-25",
  "total": 50,
  "items": [
    {
      "title": "…",
      "url": "/artigos/slug/",
      "description": "…",
      "category": "Marketing Local",
      "keywords": ["…"]
    }
  ]
}
```

O arquivo é gerado sem indentação, para reduzir o tamanho. Ele contém apenas metadados — não o corpo dos artigos —, o que o mantém leve e faz da busca um localizador de páginas, não um mecanismo de texto completo.

`/busca/` é marcada como `noindex, follow` e bloqueada em `robots.txt`, porque páginas de resultado interno não devem competir no índice dos buscadores.

---

## 6. Convenções de código

### 6.1 HTML

- HTML5 semântico: `header`, `nav`, `main`, `article`, `section`, `aside`, `footer`, `figure`, `time`
- Um `<h1>` por página; hierarquia sem saltos
- `main` com `id="conteudo"`, alvo do *skip link*
- Toda `<img>` com `alt`, `width` e `height` — o `width`/`height` reserva o espaço e evita deslocamento de layout
- `loading="lazy"` e `decoding="async"` em todas as imagens, **exceto** a imagem destacada do artigo, que é a maior candidata a LCP e recebe `fetchpriority="high"`
- Links externos com `rel="noopener"`; links de compartilhamento também com `nofollow`
- ARIA apenas quando o HTML nativo não basta: `aria-label` em `nav` (há mais de uma por página), `aria-current="page"`, `aria-expanded` no botão do menu, `role="status"` nas áreas de mensagem
- Datas sempre em `<time datetime="YYYY-MM-DD">`
- Formulários com `<label>` associado, visualmente oculto quando o contexto já é evidente

### 6.2 CSS

Arquivo único, organizado em doze seções numeradas: tokens, reset, utilitários, cabeçalho, rodapé, cards e grids, home, artigo, páginas internas, responsivo, preferências do usuário e impressão.

- **Custom properties** em `:root` para cor, tipografia, espaço e forma
- **Modo escuro** por `prefers-color-scheme`, redefinindo apenas os tokens
- **Tipografia fluida** com `clamp()`, em escala de `--step--1` a `--step-4`
- **Layout** com Grid e Flexbox; nenhuma dependência de framework
- **Nomenclatura** em BEM simplificado: `.bloco`, `.bloco__elemento`, `.bloco--variante`
- **Cor de categoria** injetada por `--cat-color` no elemento, consumida por `.kicker`, `.section__head` e bordas
- **Sem `!important`**, exceto em `[hidden]` e nas regras de impressão
- **Foco visível** com `:focus-visible`, contorno de 3px na cor de destaque
- **`prefers-reduced-motion`** desliga transições e rolagem suave
- **`prefers-contrast: more`** reforça os tokens de linha e texto secundário
- **Impressão** remove cabeçalho, navegação, rodapé, índice, compartilhamento, relacionados e barra lateral; expõe o `href` dos links do corpo e acrescenta um rodapé com a origem do conteúdo

### 6.3 JavaScript

- ES6+ em IIFE com `'use strict'`, sem módulos e sem etapa de build
- Carregado com `defer`
- **Nunca** monta conteúdo editorial
- Toda funcionalidade é verificada antes do uso (`navigator.clipboard`, `navigator.share`, `matchMedia`, `serviceWorker`)
- Elementos que dependem de API só aparecem quando a API existe (`button[hidden]` revelado por JS)
- Listeners de rolagem com `{ passive: true }` e `requestAnimationFrame` para evitar travamento
- Sem `innerHTML`, `document.write`, `eval` ou `new Function`
- Falhas são silenciosas por design: o site funciona sem nenhum dos recursos

### 6.4 Nomenclatura de arquivos

- Artigos de origem: `NN-slug-do-artigo.md`, com `NN` sequencial e `slug` idêntico ao campo do front matter
- URLs sempre com barra final
- Slugs em minúsculas, sem acento, separados por hífen, derivados do título e estáveis: **um slug publicado nunca muda** (mudá-lo quebra links internos e externos)

---

## 7. Acessibilidade

O projeto tem como alvo o nível AA das WCAG 2.2.

| Critério | Implementação |
|---|---|
| Contraste | Paleta com razão mínima de 4,5:1 para texto; a cor de destaque só é usada como texto na variante escura `--accent-text` |
| Foco visível | `:focus-visible` com contorno de 3px e deslocamento |
| Navegação por teclado | Ordem natural do DOM; menu mobile fecha com `Escape` |
| Skip link | Primeiro elemento focável, aponta para `#conteudo` |
| Estrutura | Landmarks semânticos; `nav` distintas rotuladas por `aria-label` |
| Imagens | `alt` descritivo; imagens puramente decorativas com `alt=""` e `aria-hidden="true"` |
| Movimento | `prefers-reduced-motion` desliga transições, rolagem suave e a barra de progresso |
| Formulários | `label` associado a todo campo |
| Status dinâmico | `role="status"` com `aria-live="polite"` |
| Idioma | `lang="pt-BR"` no `<html>` |
| Zoom | Sem `maximum-scale`; layout responsivo até 320 px |

O link do card que envolve a imagem recebe `tabindex="-1"` e `aria-hidden="true"` porque duplica o link do título — isso evita dois alvos de tabulação para o mesmo destino.

---

## 8. Desempenho

Decisões que sustentam o carregamento rápido:

- HTML servido pronto: sem requisição de dados para exibir conteúdo
- Um arquivo CSS, sem `@import` e sem fonte externa
- Pilha de fontes do sistema: zero download de tipografia
- `width`/`height` em todas as imagens, eliminando deslocamento de layout
- `aspect-ratio` nos containers de mídia
- Imagem destacada com `fetchpriority="high"` e sem `loading="lazy"`
- Demais imagens com `loading="lazy"` e `decoding="async"`
- Scripts com `defer`
- Imagens destacadas em SVG (menos de 1 KB cada)
- Service worker acelerando visitas subsequentes sem comprometer a atualização

Ao ativar AdSense, revalide a estabilidade visual: anúncios automáticos são a principal causa de deslocamento em sites estáticos.

---

## 8.1 Cabeçalhos de produção (`_headers`)

O arquivo `_headers` na raiz segue o formato do Cloudflare Pages e define cabeçalhos de segurança e política de cache. Em outras hospedagens, replique as mesmas regras na configuração do servidor.

### Segurança

| Cabeçalho | Valor | Motivo |
|---|---|---|
| `X-Content-Type-Options` | `nosniff` | Impede que o navegador reinterprete o tipo do arquivo |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limita o que é enviado ao sair do site |
| `X-Frame-Options` | `SAMEORIGIN` | Impede incorporação por terceiros |
| `Permissions-Policy` | câmera, microfone, geolocalização, pagamento e USB desabilitados | O site não usa nenhuma dessas APIs |
| `Cross-Origin-Opener-Policy` | `same-origin` | Isola o contexto de navegação |
| `Content-Security-Policy` | ver abaixo | Restringe origens de script, estilo, imagem e conexão |

### A política de conteúdo

```
default-src 'self'; base-uri 'self'; object-src 'none'; form-action 'self';
frame-ancestors 'self';
script-src 'self' 'sha256-…' [domínios do Google Ads e Analytics];
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' [domínios de medição];
frame-src [domínios de anúncio];
upgrade-insecure-requests
```

Três decisões merecem explicação:

1. **`'sha256-…'` em `script-src`.** O único script inline do projeto é `document.documentElement.classList.add('js');`, presente no `<head>` para marcar a disponibilidade de JavaScript antes da primeira pintura. Ele é autorizado por hash, e não por `'unsafe-inline'`. **Se esse script mudar, o hash precisa ser recalculado:**

   ```bash
   printf "document.documentElement.classList.add('js');" | openssl dgst -sha256 -binary | openssl base64
   ```

2. **`'unsafe-inline'` em `style-src`.** Os cards e cabeçalhos de seção recebem a cor da categoria por atributo `style="--cat-color: …"`. Atributos de estilo inline exigem essa permissão. O risco é baixo: nenhum valor vem de entrada do usuário — todos são constantes definidas em `site.config.mjs`.

3. **Domínios do Google já autorizados.** As entradas de AdSense e Analytics estão na política mesmo com os scripts ainda comentados no `<head>`, para que a ativação (descrita em `README.md`) não exija mexer na CSP. Enquanto os scripts estiverem comentados, nenhuma requisição é feita a esses domínios — a autorização existe, mas não é exercida. Se, ao ativar, o console acusar bloqueio de algum domínio não previsto, acrescente-o à diretiva correspondente e republique.

### Cache

| Caminho | Política | Motivo |
|---|---|---|
| `/*.html` | `max-age=0, must-revalidate` | Conteúdo pode ser corrigido a qualquer momento |
| `/sw.js` | `max-age=0, must-revalidate` | Service worker em cache impediria a atualização do próprio cache |
| `/assets/css/*`, `/assets/js/*` | `max-age=3600, must-revalidate` | Os nomes não têm hash de conteúdo |
| `/assets/images/*`, `/assets/icons/*` | `max-age=604800` | Mudam pouco |
| `/assets/fonts/*` | `max-age=2592000` | Reservado para uso futuro |
| `/data/*` | `max-age=3600, must-revalidate` | Índice de busca acompanha as publicações |
| `/feed.xml`, `/sitemap.xml` | `max-age=3600` | Consumidos por agregadores e rastreadores |

Se um dia os nomes de CSS e JS passarem a incluir hash de conteúdo, esses dois caminhos podem receber `max-age=31536000, immutable`.

---

## 9. Auditoria

`node tools/check.mjs` percorre todas as páginas HTML geradas e verifica:

1. **Links internos** — todo `href`/`src` iniciado por `/` aponta para arquivo existente
2. **Links relativos** — proibidos; o projeto usa apenas caminhos absolutos
3. **Âncoras** — todo `#id` existe na página de destino
4. **JSON-LD** — todo bloco é JSON válido
5. **Estrutura** — presença de `h1` único, canonical, meta description e alvo do skip link
6. **Imagens** — `alt`, `width` e `height` em toda `<img>`
7. **Consistência cruzada** — título, data, URL, contagem de palavras e relacionados coincidem entre HTML, `content-manifest.json`, `search-index.json`, `sitemap.xml`, `feed.xml`, `content-plan.md` e `llms.txt`
8. **XML** — declaração presente, raiz fechada e ausência de `&` não escapado

Saída esperada: `✅ Nenhum problema encontrado.` Qualquer problema encerra com código 1, o que permite usar o script em automação.

---

## 10. Solução de problemas

| Sintoma | Causa provável | Correção |
|---|---|---|
| Build falha com "Artigo curto" | Corpo abaixo de 1.300 palavras | `node tools/wordcount.mjs` e ampliar o texto |
| Build falha com "Data ausente na sequência" | Data duplicada ou fora do intervalo | Corrigir `date:` no front matter |
| `check.mjs` acusa destino inexistente | Link para slug inexistente ou com erro de digitação | Corrigir o link no Markdown |
| Índice do artigo vazio | O texto não tem H2 | Acrescentar seções |
| Página antiga persiste no navegador | Service worker com versão em cache | Recarregar; se persistir, limpar dados do site |
| Busca não retorna nada | `search-index.json` desatualizado | Rodar `node tools/build.mjs` |
| Imagem destacada ausente | SVG não gerado para o slug novo | Rodar o build (as imagens são geradas a partir dos artigos carregados) |
| Categoria inválida no build | `category:` não existe em `site.config.mjs` | Usar um dos 10 slugs válidos |
