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.
- Repositório: github.com/kenimo49/historymap
- Demo ao vivo: kenimoto.dev/products/historymap/

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:
- Fazer fork do repositório (ou usar o Use this template)
- Reescrever o
data.yamlcom os seus dados - 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:
- GitHub Pages puro: fork, push, e
https://<voce>.github.io/historymap/aparece - Embed via iframe: o iframe com
data-historymapmais o embed.js de antes - 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.
Este artigo foi útil?