← Volver al Blog

historymap: un archivo YAML se convierte en una línea de tiempo estilo sitio corporativo

Empecé una serie llamada “weekly ship”: aplicaciones, herramientas y juegos pequeños, publicados una vez por semana. El número uno es historymap, una herramienta OSS que convierte un único archivo YAML en una página de línea de tiempo al estilo “historial de productos” de sitio corporativo.

Línea de tiempo generada por historymap: años y portadas de libros alternan entre izquierda y derecha alrededor de un eje central punteado

La demo muestra el historial de publicación de mis doce libros técnicos. El formato es el que aparece en los sitios de fabricantes industriales: un eje vertical al centro, años alternando entre izquierda y derecha, fotos de producto recortadas en círculo.

Una tabla no muestra acumulación

Cada vez que publico un libro actualizo la página de libros de este sitio. Pero esa página es una tabla. Las tablas sirven para consultar, no para contar el paso del tiempo. “Este libro salió tres meses después de aquel” solo se vuelve visible cuando los datos toman la forma de una línea de tiempo. Esa fue toda la motivación.

La configuración son tres pasos:

  1. Hacer fork del repositorio (o usar Use this template)
  2. Reescribir el data.yaml con tus propios datos
  3. Activar GitHub Pages (Source: GitHub Actions) y hacer push
title: "Ken Imoto — Tech Books History"
lang: es
layout: zigzag
theme:
  preset: navy-mono
items:
  - id: claude-code-mastery
    date: 2025-09-01
    title: "Claude Code en la práctica"
    description: "Un año de Claude Code en producción."
    image: https://example.com/images/cover.png
    link: https://example.com/books/claude-code-mastery/

Con dos campos, date y title, ya renderiza. Funciona con historiales de publicación, y también con historiales de releases de OSS, trayectorias de carrera y cronologías de proyectos de un equipo.

La salida es un HTML autocontenido en un solo archivo. El CSS y el JS van inline, sin ninguna referencia a CDN externas. El build pide Node 20+ y exactamente una dependencia, js-yaml. Como todo cabe en un archivo, funciona en cualquier lugar donde puedas soltar un archivo: GitHub Pages, un hosting compartido barato, donde sea.

El diseño en zigzag es solo flexbox

“Alternar entre izquierda y derecha” suena laborioso, pero todo se reduce a un cambio de flex-direction:

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

Repartiendo las clases entre elementos pares e impares, el texto y la imagen intercambian lados. El eje central es un único borde punteado en .timeline::before, sin imágenes de por medio.

Tres detalles exigieron atención de verdad. El recorte circular es el de siempre, border-radius: 50%, pero las portadas de libros son altas, y object-fit: cover corta arriba y abajo; lo cambié por contain dentro de un círculo blanco. Las etiquetas de año empezaron mostrando solo el año, lo que me dejó una pantalla con “2026” impreso once veces seguidas (publica un libro al mes y esto es lo que pasa), así que las fechas con precisión de mes ahora salen como 2026.03. Y por debajo de 640px el eje se va al borde izquierdo y todo colapsa a una sola columna. El zigzag solo se justifica cuando hay ancho para zigzaguear.

Altura del iframe: ResizeObserver + postMessage

El objetivo real de la herramienta es el embed vía iframe. Quería que la línea de tiempo generada entrara en un blog o portafolio con una línea. Pero los iframes tienen un problema clásico: el padre no ve la altura del contenido. Una línea de tiempo crece en vertical, así que un height="600" fijo garantiza o barra de desplazamiento o espacio vacío sobrante.

La solución no cambia desde hace años: el hijo informa su altura al padre. La página generada lleva un ResizeObserver y dispara postMessage cada vez que su altura cambia, así que sigue el ritmo incluso cuando las imágenes de carga diferida estiran la página después. Del lado del padre, el embed.js incluido recibe el mensaje y actualiza el iframe correspondiente.

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

El detalle que importa: identificar el iframe por event.source. Las implementaciones que buscan por URL se rompen apenas la misma página se incrusta dos veces. Comparando contentWindow con event.source, solo crece el iframe correcto, sin importar cuántos embeds compartan la página. Las alturas recibidas además pasan por un chequeo de Number.isFinite y un tope máximo antes de aplicarse.

La entrada es la superficie de ataque de un template, así que recházala en la puerta

Como el data.yaml se distribuye como template, es entrada escrita por desconocidos. Los valores que desembocan en href, en bloques <style> y en rutas de archivo no merecen confianza. En lugar de apoyarlo todo en el escape de la salida, el validador tumba el build ante cualquier cosa fuera de una allowlist:

  • link: cualquier esquema que no sea http / https / mailto / tel (javascript: incluido) es error
  • Colores del theme: lo que no sea formato hex es error; las fuentes pasan por una allowlist de caracteres con alfanuméricos más , . ' " -
  • image: las rutas absolutas se rechazan, y si la ruta resuelta se escapa del directorio del data.yaml, también es error

Los generadores de sitios estáticos tienen la válvula de seguridad más simple que existe: hacer fallar el build. La entrada mala nunca llega a una pantalla; se detiene ahí mismo. Eso me ahorra tener que pensar en “cómo renderizar valores maliciosos de forma segura”, y el diseño se mantiene pequeño.

Tres maneras de servirlo

Como la salida es un solo archivo, elige la que te acomode:

  1. GitHub Pages tal cual: fork, push, y aparece https://<tu-usuario>.github.io/historymap/
  2. Embed vía iframe: el iframe con data-historymap más el embed.js de antes
  3. Servir bajo tu propio dominio: la demo en vivo del comienzo funciona así. Este sitio se sirve con Cloudflare Workers, así que levanté un Worker diminuto y agregué una Route para kenimoto.dev/products/historymap/*

La v1 sale con un solo layout, zigzag, pero el renderizador queda detrás de un registry. Árboles genealógicos, estilo mapa de metro y otros layouts pueden agregarse sobre el mismo data.yaml.

Cada lanzamiento de la serie weekly ship entra en la página de productos, con su historia de vida completa: congelamientos en estático, promociones a dominio propio y retiros al archivo. Prueba poner tres entradas de tu propio historial en el data.yaml y mira cómo se lee.