[{"data":1,"prerenderedAt":547},["ShallowReactive",2],{"content-/decision-pages-and-dynamic-family-ages":3,"all-pages-for-dir":545,"og-image-/decision-pages-and-dynamic-family-ages":546},{"id":4,"title":5,"body":6,"category":527,"description":528,"extension":529,"meta":530,"navigation":531,"ogImage":532,"path":533,"project_name":534,"published":535,"publishedAt":536,"seo":537,"stem":538,"tags":539,"todo":532,"unpublished":535,"updatedAt":532,"__hash__":544},"pages/2026-05/2026-05-17/decision-pages-and-dynamic-family-ages.md","家族旅行サイトに『意思決定ページ』と動的年齢計算を仕込んだ — import.meta.glob で個人情報を防御する",{"type":7,"value":8,"toc":516},"minimark",[9,13,26,38,43,55,62,76,198,211,222,226,229,241,248,268,277,284,288,291,294,299,302,310,324,328,331,351,364,379,383,386,393,400,411,418,421,479,482,500,506,512],[10,11,12],"p",{},"夏の家族旅行（大分・別府）の準備サイトを Astro で組み立てている。今日は3つを Claude Code に実装させた。",[14,15,16,20,23],"ol",{},[17,18,19],"li",{},"家族の生年月を JSON に外出しして、現在年齢を動的計算するヘルパー",[17,21,22],{},"リゾートホテル A に泊まるかどうかの意思決定を可視化する専用ページ",[17,24,25],{},"旅行詳細ページを「結論はテーブル、詳細はクリックで」方針に組み直し",[10,27,28,29,33,34,37],{},"派手な機能追加というより、",[30,31,32],"strong",{},"個人情報をどうコードから切り離すか","と、",[30,35,36],{},"意思決定をどう紙の上から画面に持ち込むか","の話に寄っている。",[39,40,42],"h2",{"id":41},"動的年齢計算-来年実行しても自動で正しいを仕込む","動的年齢計算: 「来年実行しても自動で正しい」を仕込む",[10,44,45,46,50,51,54],{},"家族メンバーの誕生月の Excel を渡して、",[47,48,49],"code",{},"src/data/family.json","（年と月だけ）に起こさせた。年齢計算のヘルパーは ",[47,52,53],{},"src/lib/family.ts"," に置いて、旅行開始日を渡すと「その時点で何歳か」を返す純粋関数にした。",[10,56,57,58,61],{},"ここで一つ判断したのが、",[30,59,60],{},"家族の生年月は GitHub に上げない","こと。個人情報なので当然なのだが、Astro のビルドは「JSON が存在する前提」で書くと、データを置いていない別ブランチや CI でビルドが落ちる。",[10,63,64,67,68,71,72,75],{},[47,65,66],{},".gitignore"," に ",[47,69,70],{},"src/data/family*.json"," を入れた上で、ローダーを ",[47,73,74],{},"import.meta.glob"," で書かせた。",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-ts shiki shiki-themes vitesse-light vitesse-light","// あれば読む、無ければ空配列に倒す\nconst modules = import.meta.glob('../data/family*.json', { eager: true })\nconst members = Object.values(modules)[0]?.default ?? []\n","ts","",[47,84,85,94,157],{"__ignoreMap":82},[86,87,90],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"sxvE3","// あれば読む、無ければ空配列に倒す\n",[86,95,97,101,105,109,113,116,120,122,126,129,133,137,139,142,145,148,151,154],{"class":88,"line":96},2,[86,98,100],{"class":99},"stQ0i","const ",[86,102,104],{"class":103},"s4oTP","modules",[86,106,108],{"class":107},"shFtX"," =",[86,110,112],{"class":111},"sHkkW"," import",[86,114,115],{"class":107},".",[86,117,119],{"class":118},"sz8Xr","meta",[86,121,115],{"class":107},[86,123,125],{"class":124},"senZ8","glob",[86,127,128],{"class":107},"(",[86,130,132],{"class":131},"sMJiu","'",[86,134,136],{"class":135},"sdGka","../data/family*.json",[86,138,132],{"class":131},[86,140,141],{"class":107},",",[86,143,144],{"class":107}," { ",[86,146,147],{"class":118},"eager",[86,149,150],{"class":107},": ",[86,152,153],{"class":111},"true",[86,155,156],{"class":107}," })\n",[86,158,160,162,165,167,170,172,175,177,179,182,186,189,192,195],{"class":88,"line":159},3,[86,161,100],{"class":99},[86,163,164],{"class":103},"members",[86,166,108],{"class":107},[86,168,169],{"class":103}," Object",[86,171,115],{"class":107},[86,173,174],{"class":124},"values",[86,176,128],{"class":107},[86,178,104],{"class":103},[86,180,181],{"class":107},")[",[86,183,185],{"class":184},"sM54T","0",[86,187,188],{"class":107},"]?.",[86,190,191],{"class":103},"default",[86,193,194],{"class":99}," ?? ",[86,196,197],{"class":107},"[]\n",[10,199,200,203,204,206,207,210],{},[47,201,202],{},"import"," を直書きするとファイルが無い瞬間にビルドエラーになる。",[47,205,74],{}," はビルド時にマッチしたファイルだけ集めるので、",[30,208,209],{},"ファイルの有無自体を「実行時の問題」ではなく「ビルド時の柔軟性」に変換","できる。これは「個人情報を含む静的サイト」のパターンとして今後も使い回せる感触がある。",[10,212,213,214,217,218,221],{},"年齢計算自体は素朴で、旅行開始日と生年月の差を月単位で見て、誕生月を迎えたかどうかで ±1 する関数を ",[47,215,216],{},"tests/family.test.ts"," で先に書いてから本体を書かせた。来年同じスクリプトを叩いても、",[47,219,220],{},".json"," を1つ書き換えるだけで全ページの年齢表示が更新される。",[39,223,225],{"id":224},"意思決定ページ-excel-の-3-シートを-json-にして純粋関数に落とす","意思決定ページ: Excel の 3 シートを JSON にして純粋関数に落とす",[10,227,228],{},"リゾートホテル A に泊まるかどうかを、Excel で 3 シートに分けて検討してあった。",[230,231,232,235,238],"ul",{},[17,233,234],{},"基本 4 シナリオ（泊まる / 泊まらない × 2 軸）",[17,236,237],{},"体験価値を含めた 5 シナリオ",[17,239,240],{},"体験価値の分解（プール・食事・部屋…）",[10,242,243,244,247],{},"これを ",[47,245,246],{},"/trips/2026-08-oita/decisions/sugino-i"," という専用ページに移植させた。流れはシンプルで、",[14,249,250,256,262],{},[17,251,252,255],{},[47,253,254],{},"src/data/sugino-i.json"," に 3 シートを構造化して落とす",[17,257,258,261],{},[47,259,260],{},"src/lib/sugino-i.ts"," に計算ロジック（純粋関数）を切り出す",[17,263,264,267],{},[47,265,266],{},"SuginoIExperience.vue"," で体験価値の重みづけを動かせる感度分析ツールにする",[10,269,270,272,273,276],{},[47,271,260],{}," は副作用を含まない。引数だけ見て結果を返すので、",[47,274,275],{},"tests/sugino-i.test.ts"," で Excel の全数値と突き合わせて検算できた。Vue コンポーネントは「スライダーを動かしたら再計算する薄いシェル」に徹していて、ロジックは触らない。",[10,278,279,280,283],{},"体験価値を 1.0 倍から動かすと「お得率」が連動して動く。紙の Excel だと「もう一回シミュレーションお願いします」と上司に頼む感覚だったやつが、画面の上でスライダーを掴んだ瞬間に答えが返る。同業者（税理士・会計士）の業務に重ねるなら、",[30,281,282],{},"節税シミュレーションを Excel から SaaS に持ち上げたとき","の質的変化に近い。",[39,285,287],{"id":286},"旅行詳細ページの大改造-テーブル-右ドロワーのハイブリッド","旅行詳細ページの大改造: テーブル + 右ドロワーのハイブリッド",[10,289,290],{},"最初、旅行詳細ページは縦に長くスクロールする構造だった。決定事項・宿泊候補・観光メモが全部地の文で並んでいて、一画面で全体像を掴めない。",[10,292,293],{},"「結論をテーブルで一覧、詳細はクリックで」の方針で組み直させた。",[295,296,298],"h3",{"id":297},"決定事項テーブルを-6-列に細分化","決定事項テーブルを 6 列に細分化",[10,300,301],{},"最初は「結論」1 列だけだった。「結論だと雑すぎる」というツッコミを自分で入れて、段階的に分解した。",[230,303,304,307],{},[17,305,306],{},"第 1 段階: 「項目 / 状態 / 結論 / 詳細」の 4 列",[17,308,309],{},"第 2 段階: 「期間」「金額」を別カラムに切り出して 6 列に",[10,311,312,315,316,319,320,323],{},[47,313,314],{},"amount"," と ",[47,317,318],{},"period"," をスキーマ（frontmatter）に追加して、Markdown 側で構造化されたデータを持てるようにした。結論文に金額を文字列で混ぜていた状態から、",[30,321,322],{},"カラムを分けた瞬間に「比較したい軸」が露わになる","。これはテーブルの本質的な効能で、Excel を毎日触る人なら身体感覚で分かるやつ。",[295,325,327],{"id":326},"右ドロワー-vs-ページ遷移-vs-モーダル","右ドロワー vs ページ遷移 vs モーダル",[10,329,330],{},"短い検討メモを「クリックして開く」表現にする時、選択肢が 3 つあった。",[230,332,333,339,345],{},[17,334,335,338],{},[30,336,337],{},"ページ遷移",": 戻るボタンで戻る。読み物として落ち着く。が、メインの旅程画面が消える",[17,340,341,344],{},[30,342,343],{},"モーダル",": 中央にポップアップ。が、縦に長い読み物だとスクロールが詰まる",[17,346,347,350],{},[30,348,349],{},"右ドロワー",": 横からシュッと出てくる。メイン画面が左に残る",[10,352,353,354,356,357,360,361,363],{},"迷った末、",[30,355,349],{},"を採用した。決め手は「ここがメインだよ、というのが視覚的に残る」こと。",[47,358,359],{},"\u003Cdialog>"," 要素 + CSS スライドアニメーション + 最小限の JS で組ませた。アクセシビリティ的にもネイティブ ",[47,362,359],{}," を使うと ESC キーで閉じる挙動などがブラウザに任せられる。",[10,365,366,367,370,371,374,375,378],{},"ただし全てをドロワーにしたわけではなく、",[30,368,369],{},"リンク先のページがある決定事項は別ページ遷移、短い検討メモだけのものはドロワー","というハイブリッド構成にした。",[47,372,373],{},"decisions"," スキーマに ",[47,376,377],{},"link"," フィールドを足して、フィールドの有無で分岐させている。",[39,380,382],{"id":381},"交通手段の検討-業界標準の-3-層モデルを判断軸に借りた","交通手段の検討: 業界標準の 3 層モデルを判断軸に借りた",[10,384,385],{},"旅行サイトに直接関係する話ではないが、同じ日に「ソラシドエア + トヨタレンタカーをセットで予約するか別々か」をリサーチさせた。検討結果をテーブル化して旅行ページの検討メモに差し込んだので、流れの記録として残しておく。",[10,387,388,389,392],{},"5 パターン（航空券単体・パック・福岡経由・長崎経由 等）を比較した結果、",[30,390,391],{},"既存案（羽田 ⇔ 大分直行 + レンタカー別予約）が最安","で、福岡経由は数万円差のレンジで割高。最初は「パックにしたら安いはず」という直感だったが、車種指定（7 人乗り HEV）と早割が既に効いていて、パックで上書きすると上振れする構造だった。",[10,394,395,396,399],{},"途中で「業界標準のアプローチを調べてから判断軸を組み立てて」と指示を入れたら、",[30,397,398],{},"募集型パッケージ／ダイナミックパッケージ／FIT（個別手配）の 3 層","で整理されているという話が返ってきた。",[230,401,402,405,408],{},[17,403,404],{},"募集型パッケージ: 旅行会社が組んだセット商品",[17,406,407],{},"ダイナミックパッケージ: 航空券 + 宿 + レンタカーを自分で組むセット",[17,409,410],{},"FIT: 全部バラで個別手配",[10,412,413,414,417],{},"今回は「宿泊の大半が実家泊」という特殊事情があって、DP の旨味が削がれる。",[30,415,416],{},"自前の判断より、業界の整理を借りた方が早く・正しく決まる","。これは税務でも法務でも同じで、「業界の標準的な型」を最初に引っ張ってきてから個別事情を当て込むのが、結局いちばん速い。",[39,419,420],{"id":420},"試行錯誤",[422,423,424,437],"table",{},[425,426,427],"thead",{},[428,429,430,434],"tr",{},[431,432,433],"th",{},"試したこと",[431,435,436],{},"結果",[438,439,440,455,463,471],"tbody",{},[428,441,442,452],{},[443,444,445,446,448,449,451],"td",{},"家族データを ",[47,447,66],{}," で隔離 + ",[47,450,74],{}," で防御的にロード",[443,453,454],{},"データ無しでもビルドが通る構造になり、CI / 別ブランチでの事故をゼロに",[428,456,457,460],{},[443,458,459],{},"決定事項テーブルを「結論」1 列で書き始めた",[443,461,462],{},"雑すぎたので「期間」「金額」を別カラムに切り出して 6 列化、比較軸が見えるようになった",[428,464,465,468],{},[443,466,467],{},"検討メモをページ遷移にするかモーダルにするか右ドロワーにするか",[443,469,470],{},"右ドロワーを選択。メイン画面が左に残るので「ここが主役」が消えない",[428,472,473,476],{},[443,474,475],{},"パック予約と単独予約をレンジ試算で比較",[443,477,478],{},"業界標準 3 層（募集型 / DP / FIT）を判断軸に借りて、結局現状（単独予約）が最安と確定",[39,480,481],{"id":481},"学び",[10,483,484,493,494,496,497,499],{},[30,485,486,487,489,490,492],{},"個人情報を扱う静的サイトでは、",[47,488,66],{}," + ",[47,491,74],{}," の組み合わせが効く。"," ",[47,495,202],{}," を直書きするとファイルが無い瞬間にビルドエラーになるが、",[47,498,74],{}," でラップすると「あれば読む、無ければ空配列」が一行で書ける。データの有無自体をビルドのスイッチに使える、と覚えておく。",[10,501,502,505],{},[30,503,504],{},"意思決定は Excel から HTML に持ち上げた瞬間に資産になる。"," 紙の Excel 上の検討は、相手に渡した瞬間に固まる。HTML 上の感度分析は、相手がスライダーを動かして自分で答えを引き出せる。家族の旅行という卑近な題材だが、税務シミュレーションや資金繰り検討にそのまま転用できる構造だと感じた。",[10,507,508,511],{},[30,509,510],{},"意思決定の UI は、結論をテーブルに、検討メモを右ドロワーに分けると一画面で全体像が握れる。"," スクロールで縦に伸びる構造より、一覧性が明確に勝つ。",[513,514,515],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":82,"searchDepth":96,"depth":96,"links":517},[518,519,520,524,525,526],{"id":41,"depth":96,"text":42},{"id":224,"depth":96,"text":225},{"id":286,"depth":96,"text":287,"children":521},[522,523],{"id":297,"depth":159,"text":298},{"id":326,"depth":159,"text":327},{"id":381,"depth":96,"text":382},{"id":420,"depth":96,"text":420},{"id":481,"depth":96,"text":481},"dev","宿泊先を選ぶ意思決定をテーブルで可視化し、家族の生年月から年齢を動的計算するヘルパーを Astro サイトに組み込んだ。個人情報は .gitignore で除外し、import.meta.glob で『あれば読む、無ければ空配列』に倒した記録。","md",{},true,null,"/decision-pages-and-dynamic-family-ages","family-trips",false,"2026-05-17T00:00:00.000Z",{"title":5,"description":528},"2026-05/2026-05-17/decision-pages-and-dynamic-family-ages",[534,540,541,542,543],"astro","vue","import-meta-glob","decision-making","iH1S5WS_LlsZK-1m8IqhrV7jO9K9AcMzCXPrRhhgUyY",[],"https://log.eurekapu.com/og/blog/decision-pages-and-dynamic-family-ages.png?v=2026-05-17T00%3A00%3A00.000Z&title=%E5%AE%B6%E6%97%8F%E6%97%85%E8%A1%8C%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AB%E3%80%8E%E6%84%8F%E6%80%9D%E6%B1%BA%E5%AE%9A%E3%83%9A%E3%83%BC%E3%82%B8%E3%80%8F%E3%81%A8%E5%8B%95%E7%9A%84%E5%B9%B4%E9%BD%A2%E8%A8%88%E7%AE%97%E3%82%92%E4%BB%95%E8%BE%BC%E3%82%93%E3%81%A0%20%E2%80%94%20import.meta.glob%20%E3%81%A7%E5%80%8B%E4%BA%BA%E6%83%85%E5%A0%B1%E3%82%92%E9%98%B2%E5%BE%A1%E3%81%99%E3%82%8B&author=Kei%20Komatsu&sig=79159dcd69e08630",1782528837411]