YAMLを1枚書くと企業サイト風の年表ページになるOSS「historymap」を公開した
小さなアプリ・ツール・ゲームを週次で公開する「weekly ship」シリーズを始めました。第1号は historymap。YAMLを1枚書くと、企業サイト風の製品ヒストリー年表ページを生成するOSSです。

デモの中身は私の技術書12冊の出版履歴です。産業機器メーカーのサイトによくある、中央に縦軸が通って年号が左右交互に並ぶあの形式を、そのまま自分のデータで再現できます。
表は積み重ねを語らない
私は本を出すたびにサイトの書籍一覧を更新していますが、一覧はあくまで「表」です。表は検索には向いていても、「この本の3ヶ月後にこの本が出た」という時間の流れは見えません。積み重ねは、年表の形をしていないと伝わりません。それが作った動機です。
使い方は3ステップにしました。
- リポジトリをfork(またはUse this template)
data.yamlを自分のデータに書き換える- GitHub Pagesを有効化(Source: GitHub Actions)して push
title: "Ken Imoto — Tech Books History"
lang: ja
layout: zigzag
theme:
preset: navy-mono
items:
- id: claude-code-mastery
date: 2025-09-01
title: "実践Claude Code"
description: "Claude Code を1年以上、実務で使い込んだ。"
image: https://example.com/images/cover.png
link: https://example.com/books/claude-code-mastery/
date と title の2項目だけでも動きます。出版履歴のほか、OSSのリリース史、キャリア年表、チームのプロジェクト史あたりは書き換えるだけで入ります。
生成物は self-containedなHTML 1ファイルです。CSSもJSもインラインで、外部CDN参照はゼロ。ビルドはNode 20+、依存パッケージは js-yaml の1個だけにしました。1ファイルに畳んであれば、GitHub Pagesでもレンタルサーバーでも置くだけで動きます。
左右交互レイアウトはflexboxで足りる
「ジグザグ配置」と聞くと面倒そうですが、芯は flex-direction の切り替えだけです。
.item--left { flex-direction: row; }
.item--right { flex-direction: row-reverse; }
奇数番目と偶数番目でこのクラスを振り分ければ、テキストと画像の位置が入れ替わります。中央の縦軸も .timeline::before に破線ボーダーを1本引くだけで、画像は使っていません。
細部で考えたのは3点です。書影の丸抜きは定石どおり border-radius: 50% ですが、縦長の表紙を object-fit: cover に通すと上下が切れるので、白背景の円に contain で収める方式にしました。年ラベルは最初、年だけの表示にしていたら「2026」が11連続する画面になったので(毎月本を出すとこうなります)、月精度の日付は 2026.03 形式に変えました。モバイルは640px以下で軸を左端に寄せて単列に畳みます。ジグザグは幅があってこそなので、狭い画面では維持しません。
iframeの高さはResizeObserver + postMessage
このツールの本命はiframe埋め込みです。生成した年表を、自分のブログやポートフォリオに1行で足せるのがゴールです。ところがiframeには古典的な問題があります。中身の高さが親から見えないことです。年表は縦に伸びるコンテンツなので、height="600" のような固定値では必ずスクロールバーか余白が出ます。
解決は昔から変わらず、子から親への高さ通知です。生成ページ側に ResizeObserver を仕込み、高さが変わるたびに postMessage で親へ送ります。画像の遅延読み込みで後から高さが伸びても追従できます。親側は同梱の embed.js がメッセージを受けて該当iframeの高さを更新します。
<iframe data-historymap src="https://your-name.github.io/historymap/" style="width:100%;border:0"></iframe>
<script src="embed.js"></script>
実装のポイントは event.source でiframeを特定することです。URLで探す実装だと、同じページを2箇所に埋め込んだときに壊れます。contentWindow と event.source の一致で見れば、複数埋め込みでも正しいiframeだけが伸びます。受信した高さも Number.isFinite チェックと上限クランプを通してから適用しています。
入力はテンプレ配布物の攻撃面なので、入口で落とす
data.yamlはテンプレートとして配る以上、他人が書く入力です。href・<style>・ファイルパスに流れ込む値をそのまま信用はできません。そこで出力時のエスケープに頼らず、バリデーション段階でallowlistに合わない値をビルドエラーにする方針にしました。
link: http / https / mailto / tel 以外のスキーム(javascript:等)はエラーthemeの色: hex形式以外はエラー。fontは英数と, . ' " -だけの文字allowlistimage: 絶対パスを拒否し、解決結果がdata.yamlのあるディレクトリの外に出たらエラー
静的サイトジェネレータには、ビルドを失敗させるという一番単純な安全弁があります。おかしな入力は画面に出さず、その場で止める。この方針だと「不正な値をどう安全に出力するか」を考えずに済むので、設計が小さいまま保てます。
配信は3パターン
生成物が1ファイルなので、置き方は好みで選べます。
- GitHub Pagesをそのまま使う: forkしてpushすれば
https://<you>.github.io/historymap/が生えます - iframeで自分のサイトに埋め込む: 先ほどの
data-historymap付きiframe + embed.js - 自分のドメインのパス配下で配信する: 冒頭のライブデモがこの方式です。このサイトはCloudflare Workers配信なので、小さなWorkerを立てて
kenimoto.dev/products/historymap/*にRouteを1本張りました
レイアウトはv1では zigzag の1種類ですが、レンダラーはレジストリ方式で分離してあります。系譜図や路線図のような別レイアウトを、同じdata.yamlに足せる構造です。
weekly shipのリリースはプロダクト一覧に、静的化・独立ドメイン昇格・アーカイブの遍歴ごと積んでいきます。まずは手元の時系列データを3件ほど data.yaml に入れて眺めてみてください。
この記事は役に立ちましたか?