夏の家族旅行(大分・別府)の準備サイトを Astro で組み立てている。今日は3つを Claude Code に実装させた。
- 家族の生年月を JSON に外出しして、現在年齢を動的計算するヘルパー
- リゾートホテル A に泊まるかどうかの意思決定を可視化する専用ページ
- 旅行詳細ページを「結論はテーブル、詳細はクリックで」方針に組み直し
派手な機能追加というより、個人情報をどうコードから切り離すかと、意思決定をどう紙の上から画面に持ち込むかの話に寄っている。
動的年齢計算: 「来年実行しても自動で正しい」を仕込む
家族メンバーの誕生月の Excel を渡して、src/data/family.json(年と月だけ)に起こさせた。年齢計算のヘルパーは src/lib/family.ts に置いて、旅行開始日を渡すと「その時点で何歳か」を返す純粋関数にした。
ここで一つ判断したのが、家族の生年月は GitHub に上げないこと。個人情報なので当然なのだが、Astro のビルドは「JSON が存在する前提」で書くと、データを置いていない別ブランチや CI でビルドが落ちる。
.gitignore に src/data/family*.json を入れた上で、ローダーを import.meta.glob で書かせた。
// あれば読む、無ければ空配列に倒す
const modules = import.meta.glob('../data/family*.json', { eager: true })
const members = Object.values(modules)[0]?.default ?? []
import を直書きするとファイルが無い瞬間にビルドエラーになる。import.meta.glob はビルド時にマッチしたファイルだけ集めるので、ファイルの有無自体を「実行時の問題」ではなく「ビルド時の柔軟性」に変換できる。これは「個人情報を含む静的サイト」のパターンとして今後も使い回せる感触がある。
年齢計算自体は素朴で、旅行開始日と生年月の差を月単位で見て、誕生月を迎えたかどうかで ±1 する関数を tests/family.test.ts で先に書いてから本体を書かせた。来年同じスクリプトを叩いても、.json を1つ書き換えるだけで全ページの年齢表示が更新される。
意思決定ページ: Excel の 3 シートを JSON にして純粋関数に落とす
リゾートホテル A に泊まるかどうかを、Excel で 3 シートに分けて検討してあった。
- 基本 4 シナリオ(泊まる / 泊まらない × 2 軸)
- 体験価値を含めた 5 シナリオ
- 体験価値の分解(プール・食事・部屋…)
これを /trips/2026-08-oita/decisions/sugino-i という専用ページに移植させた。流れはシンプルで、
src/data/sugino-i.jsonに 3 シートを構造化して落とすsrc/lib/sugino-i.tsに計算ロジック(純粋関数)を切り出すSuginoIExperience.vueで体験価値の重みづけを動かせる感度分析ツールにする
src/lib/sugino-i.ts は副作用を含まない。引数だけ見て結果を返すので、tests/sugino-i.test.ts で Excel の全数値と突き合わせて検算できた。Vue コンポーネントは「スライダーを動かしたら再計算する薄いシェル」に徹していて、ロジックは触らない。
体験価値を 1.0 倍から動かすと「お得率」が連動して動く。紙の Excel だと「もう一回シミュレーションお願いします」と上司に頼む感覚だったやつが、画面の上でスライダーを掴んだ瞬間に答えが返る。同業者(税理士・会計士)の業務に重ねるなら、節税シミュレーションを Excel から SaaS に持ち上げたときの質的変化に近い。
旅行詳細ページの大改造: テーブル + 右ドロワーのハイブリッド
最初、旅行詳細ページは縦に長くスクロールする構造だった。決定事項・宿泊候補・観光メモが全部地の文で並んでいて、一画面で全体像を掴めない。
「結論をテーブルで一覧、詳細はクリックで」の方針で組み直させた。
決定事項テーブルを 6 列に細分化
最初は「結論」1 列だけだった。「結論だと雑すぎる」というツッコミを自分で入れて、段階的に分解した。
- 第 1 段階: 「項目 / 状態 / 結論 / 詳細」の 4 列
- 第 2 段階: 「期間」「金額」を別カラムに切り出して 6 列に
amount と period をスキーマ(frontmatter)に追加して、Markdown 側で構造化されたデータを持てるようにした。結論文に金額を文字列で混ぜていた状態から、カラムを分けた瞬間に「比較したい軸」が露わになる。これはテーブルの本質的な効能で、Excel を毎日触る人なら身体感覚で分かるやつ。
右ドロワー vs ページ遷移 vs モーダル
短い検討メモを「クリックして開く」表現にする時、選択肢が 3 つあった。
- ページ遷移: 戻るボタンで戻る。読み物として落ち着く。が、メインの旅程画面が消える
- モーダル: 中央にポップアップ。が、縦に長い読み物だとスクロールが詰まる
- 右ドロワー: 横からシュッと出てくる。メイン画面が左に残る
迷った末、右ドロワーを採用した。決め手は「ここがメインだよ、というのが視覚的に残る」こと。<dialog> 要素 + CSS スライドアニメーション + 最小限の JS で組ませた。アクセシビリティ的にもネイティブ <dialog> を使うと ESC キーで閉じる挙動などがブラウザに任せられる。
ただし全てをドロワーにしたわけではなく、リンク先のページがある決定事項は別ページ遷移、短い検討メモだけのものはドロワーというハイブリッド構成にした。decisions スキーマに link フィールドを足して、フィールドの有無で分岐させている。
交通手段の検討: 業界標準の 3 層モデルを判断軸に借りた
旅行サイトに直接関係する話ではないが、同じ日に「ソラシドエア + トヨタレンタカーをセットで予約するか別々か」をリサーチさせた。検討結果をテーブル化して旅行ページの検討メモに差し込んだので、流れの記録として残しておく。
5 パターン(航空券単体・パック・福岡経由・長崎経由 等)を比較した結果、既存案(羽田 ⇔ 大分直行 + レンタカー別予約)が最安で、福岡経由は数万円差のレンジで割高。最初は「パックにしたら安いはず」という直感だったが、車種指定(7 人乗り HEV)と早割が既に効いていて、パックで上書きすると上振れする構造だった。
途中で「業界標準のアプローチを調べてから判断軸を組み立てて」と指示を入れたら、募集型パッケージ/ダイナミックパッケージ/FIT(個別手配)の 3 層で整理されているという話が返ってきた。
- 募集型パッケージ: 旅行会社が組んだセット商品
- ダイナミックパッケージ: 航空券 + 宿 + レンタカーを自分で組むセット
- FIT: 全部バラで個別手配
今回は「宿泊の大半が実家泊」という特殊事情があって、DP の旨味が削がれる。自前の判断より、業界の整理を借りた方が早く・正しく決まる。これは税務でも法務でも同じで、「業界の標準的な型」を最初に引っ張ってきてから個別事情を当て込むのが、結局いちばん速い。
試行錯誤
| 試したこと | 結果 |
|---|---|
家族データを .gitignore で隔離 + import.meta.glob で防御的にロード | データ無しでもビルドが通る構造になり、CI / 別ブランチでの事故をゼロに |
| 決定事項テーブルを「結論」1 列で書き始めた | 雑すぎたので「期間」「金額」を別カラムに切り出して 6 列化、比較軸が見えるようになった |
| 検討メモをページ遷移にするかモーダルにするか右ドロワーにするか | 右ドロワーを選択。メイン画面が左に残るので「ここが主役」が消えない |
| パック予約と単独予約をレンジ試算で比較 | 業界標準 3 層(募集型 / DP / FIT)を判断軸に借りて、結局現状(単独予約)が最安と確定 |
学び
個人情報を扱う静的サイトでは、.gitignore + import.meta.glob の組み合わせが効く。 import を直書きするとファイルが無い瞬間にビルドエラーになるが、import.meta.glob でラップすると「あれば読む、無ければ空配列」が一行で書ける。データの有無自体をビルドのスイッチに使える、と覚えておく。
意思決定は Excel から HTML に持ち上げた瞬間に資産になる。 紙の Excel 上の検討は、相手に渡した瞬間に固まる。HTML 上の感度分析は、相手がスライダーを動かして自分で答えを引き出せる。家族の旅行という卑近な題材だが、税務シミュレーションや資金繰り検討にそのまま転用できる構造だと感じた。
意思決定の UI は、結論をテーブルに、検討メモを右ドロワーに分けると一画面で全体像が握れる。 スクロールで縦に伸びる構造より、一覧性が明確に勝つ。