← Voltar ao Blog

historymap: um arquivo YAML vira uma linha do tempo estilo site corporativo

Comecei uma série chamada “weekly ship”: aplicativos, ferramentas e jogos pequenos, publicados uma vez por semana. O número um é o historymap, uma ferramenta OSS que transforma um único arquivo YAML em uma página de linha do tempo no estilo “histórico de produtos” de site corporativo.

Linha do tempo gerada pelo historymap: anos e capas de livros alternam entre esquerda e direita ao redor de um eixo central tracejado

A demo mostra o histórico de publicação dos meus doze livros técnicos. O formato é aquele que aparece em site de fabricante industrial: um eixo vertical no centro, anos alternando entre esquerda e direita, fotos de produto recortadas em círculo.

Tabela não mostra acúmulo

Toda vez que publico um livro, atualizo a página de livros deste site. Só que aquela página é uma tabela. Tabela serve para consulta, mas não conta a passagem do tempo. “Este livro saiu três meses depois daquele” só fica visível quando os dados tomam a forma de uma linha do tempo. Foi essa a motivação inteira.

A configuração são três passos:

  1. Fazer fork do repositório (ou usar o Use this template)
  2. Reescrever o data.yaml com os seus dados
  3. Ativar o GitHub Pages (Source: GitHub Actions) e dar push
title: "Ken Imoto — Tech Books History"
lang: pt
layout: zigzag
theme:
  preset: navy-mono
items:
  - id: claude-code-mastery
    date: 2025-09-01
    title: "Claude Code na prática"
    description: "Um ano de Claude Code em produção."
    image: https://example.com/images/cover.png
    link: https://example.com/books/claude-code-mastery/

Dois campos, date e title, já bastam para renderizar. Funciona com histórico de publicações, mas também com histórico de releases de OSS, trajetória de carreira e cronologia de projetos de um time.

A saída é um HTML autocontido em um único arquivo. CSS e JS ficam inline, sem nenhuma referência a CDN externa. O build pede Node 20+ e exatamente uma dependência, o js-yaml. Como tudo cabe em um arquivo, roda em qualquer lugar onde dê para largar um arquivo: GitHub Pages, hospedagem compartilhada barata, onde for.

O layout em zigue-zague é só flexbox

“Alternar entre esquerda e direita” parece trabalhoso, mas o núcleo é uma troca de flex-direction:

.item--left  { flex-direction: row; }
.item--right { flex-direction: row-reverse; }

Distribuindo as classes entre itens pares e ímpares, o texto e a imagem trocam de lado. O eixo central é uma única borda tracejada em .timeline::before, sem imagem nenhuma.

Três detalhes exigiram atenção de verdade. O recorte circular é o de sempre, border-radius: 50%, mas capa de livro é alta, e object-fit: cover corta o topo e a base; troquei por contain dentro de um círculo branco. Os rótulos de ano começaram mostrando só o ano, o que me deu uma tela com “2026” impresso onze vezes seguidas (publique um livro por mês e é isso que acontece), então datas com precisão de mês agora saem como 2026.03. E abaixo de 640px o eixo vai para a borda esquerda e tudo desaba para uma coluna só. Zigue-zague só compensa quando existe largura para ziguezaguear.

Altura do iframe: ResizeObserver + postMessage

O objetivo real da ferramenta é o embed via iframe. Eu queria que a linha do tempo gerada entrasse em um blog ou portfólio com uma linha. Mas iframe tem um problema clássico: o pai não enxerga a altura do conteúdo. Linha do tempo cresce na vertical, então um height="600" fixo garante ou barra de rolagem ou espaço vazio sobrando.

A solução não muda há anos: o filho informa a própria altura ao pai. A página gerada embute um ResizeObserver e dispara postMessage sempre que a altura muda, então acompanha até quando imagens com carregamento tardio esticam a página depois. Do lado do pai, o embed.js incluído recebe a mensagem e atualiza o iframe correspondente.

<iframe data-historymap src="https://your-name.github.io/historymap/" style="width:100%;border:0"></iframe>
<script src="embed.js"></script>

O detalhe que importa: identificar o iframe pelo event.source. Implementações que procuram por URL quebram assim que a mesma página é embutida duas vezes. Comparando contentWindow com event.source, só o iframe certo cresce, não importa quantos embeds dividam a página. As alturas recebidas ainda passam por um teste de Number.isFinite e um teto máximo antes de serem aplicadas.

A entrada é a superfície de ataque de um template, então barre na porta

Como o data.yaml é distribuído como template, ele é entrada escrita por estranhos. Valores que desembocam em href, em blocos <style> e em caminhos de arquivo não merecem confiança. Em vez de apoiar tudo no escape da saída, o validador derruba o build para qualquer coisa fora de uma allowlist:

  • link: qualquer esquema que não seja http / https / mailto / tel (javascript: incluído) é erro
  • Cores do theme: o que não for formato hex é erro; fontes passam por uma allowlist de caracteres com alfanuméricos mais , . ' " -
  • image: caminhos absolutos são rejeitados, e se o caminho resolvido escapar do diretório do data.yaml, também é erro

Gerador de site estático tem a válvula de segurança mais simples que existe: falhar o build. Entrada ruim nunca chega a uma tela; para ali mesmo. Isso me livra de pensar em “como renderizar valores maliciosos com segurança”, e o projeto continua pequeno.

Três jeitos de servir

Como a saída é um arquivo só, escolha o que encaixar melhor no seu caso:

  1. GitHub Pages puro: fork, push, e https://<voce>.github.io/historymap/ aparece
  2. Embed via iframe: o iframe com data-historymap mais o embed.js de antes
  3. Servir sob o próprio domínio: a demo ao vivo do começo funciona assim. Este site é servido por Cloudflare Workers, então subi um Worker minúsculo e adicionei uma Route para kenimoto.dev/products/historymap/*

A v1 sai com um layout único, o zigzag, mas o renderizador fica atrás de um registry. Árvores genealógicas, estilo mapa de metrô e outros layouts podem ser adicionados sobre o mesmo data.yaml.

Cada lançamento da série weekly ship entra na página de produtos, com a história de vida completa: congelamentos em estático, promoções a domínio próprio e aposentadorias no arquivo. Experimente colocar três itens do seu próprio histórico no data.yaml e veja como fica.