家族のために旅程アプリを作った — 中にAIアシスタントを入れて
- カテゴリ
- AIとローカルLLM
- 公開日
- 2026年7月11日
- 著者
- Jacob Lloyd — プロジェクト完了後、AIの支援を受けて執筆
- 読了時間
- 約12分で読めます
かんたんに言うと: 家族の日本旅行のためだけに、小さなウェブサイトを作りました — 各日のプラン、天気、地図を表示し、電波がなくてもスマホで動きます。さらに、旅行の質問に答えたりプランを微調整したりできる小さなAIヘルパーのボタンも付いています。この記事は、たくさんのAIの助けを借りてそれをどう作ったかの正直な話です:うまくいったこと、壊れたこと、そして壊れやすいデザインツールのエクスポートを、安全に編集でき、安全にウェブに載せられるものへどう変えたか。整理したサンプルをダウンロードして、どのブラウザでも開けます。
家族で日本を旅行することになり、全員が計画をポケットに入れて持ち歩けるようにしたいと
思いました — 一日ごとの予定、タップですぐ開ける地図、天気、そして時差ボケで駅に立ち
尽くしているときに忘れがちな細かい情報まで。そこで、私たちだけのための小さなウェブ
アプリを作りました:一日ごとのスケジュール、ライブ天気、オフライン対応を備えた
プライベートでPIN保護された旅程、そして — 私のことなので — 旅行についての質問に
答え、ちょっとした編集までできる内蔵のAIアシスタントです。
これは正直な制作記です:うまくいった部分、思わぬ形で壊れた部分、そして壊れやすい
デザインツールのエクスポートが、公開ウェブに載せても安心できるものへとどう変わったか。
末尾にはダウンロードしてどのブラウザでも開ける匿名化サンプルを用意しています。
要点
- これは何か:モバイルファーストの旅程ウェブアプリ — 日程、立ち寄り先、ライブ天気、通貨+タイムゾーンの補助、オフライン/インストール可能、そしてAIアシスタントのタブ付き。
- 肝となるアイデア:美しいが壊れやすいデザインツールのエクスポートが、スケジュール全体を1つの小さなデータファイルで駆動し、ビルドステップがアプリを再生成するようにした途端に安全に編集できるものになった。
- ゲート:クライアント側のPINは見せかけ — 本物はレート制限付きロックアウトを備えたサーバー側のセッション。
- AI:セルフホストのローカルモデルを短い綱でつないでおく(質問に答える。編集は小さく、ガード付きのものだけ)。
- 何度も繰り返した教訓:変更が効いたかを知る唯一の方法は、本物のブラウザでアプリを描画して目で見ること — ファイルが「大丈夫なはず」だと決して信じないこと。
始まりは美しいブラックボックス
見た目はAIのデザインツールで作り、エクスポートしました。出てきたのは単一の自己完結型
HTMLファイル — フォントも画像もインライン化された完全なReactアプリ — で、1ファイルで
約14MB。見た目は素晴らしく、最初のダブルクリックで動きました。
しかしそれはブラックボックスでもありました。アプリ全体がミニファイされて1つの巨大な塊に
収まり、スケジュールはその奥深くに埋もれていました。旅行内容を手で編集するとは、機械
生成コードに外科手術をするということです。一度きりなら構いません。しかし計画が変わる
たびとなると悪夢です — そして家族旅行では、計画は毎日変わります。
そこで最初の本質的な決断はアーキテクチャに関するものでした:エクスポートを編集するのを
やめる。
すべてを可能にした唯一のアイデア
14MBのバンドルに触る代わりに、スケジュール全体を小さく読みやすいデータファイル —
日程、立ち寄り先、天気メモ、立ち寄り先ごとの写真や費用を収めた1つの content.json —
に移しました。そして短いビルドステップがそのファイルを読み、手を加えていない原本の
エクスポートからアプリを再生成します。
これはプロジェクトの性格を丸ごと変えました。いまや「立ち寄り先を追加する」や「時刻を
直す」は、人間 — あるいはAIアシスタント — が安全に行えるデータファイルへの2行の編集と、
その後の再ビルドで済みます。壊れやすい部分(14MBのアプリ)は決して手で触りません。あとから
追加したウィジェットはどれも — ライブ天気カード、自分の時刻 ↔ 東京の2つ表示の時計、
双方向の通貨コンバーター、写真や駐車メモ付きの展開できる立ち寄り先 — すべて同じデータ
駆動のビルドから生成されます。
すべてのウィジェットを飲み込んだ罠:アプリは起動時に自分を作り直す
最も時間を奪われた罠を、あなたが同じ目に遭わないように書き留めておきます。
エクスポートされたアプリは起動時にページ全体を作り直します。 静的HTMLに自分の
マークアップ — 時計、バナー、何であれ — を差し込んでも、アプリがマウントした瞬間に
消し飛びます。 要素のIDは消え、getElementById は永遠に null を返し、ウィジェットは
何も告げずに現れません。さらに厄介なのは:保存済みのHTMLファイルを検索して「検証」すると、
マークアップはちゃんとそこにあるので、見た目には問題なさそうに見える点です。ただ画面
まで生き残らないだけなのです。
2つのルールがこれを完全に解決しました:
- ページではなく、アプリ自身のテンプレートに差し込む。 アプリがUIを組み立てる場所に
ウィジェットを追加すれば、アプリの一部として描画され、起動を生き延びます。 - 挙動は
documentへのイベント委譲で配線する。 アプリはタブを切り替えるたびに
セクションごと再マウントし、そのノードを破棄して作り直します。ボタンに直接付けた
リスナーは次の再描画で死にますが、documentに付けた1つの委譲リスナー(何がクリック
されたかを判定する)はすべてを生き延びます。
その帰結として、いまや私が鉄則として扱っているテストのルールがあります:WebのUIを、
ファイルをgrepしたりHTTP 200を確認したりして検証してはいけない。 起動後に本物の
(ヘッドレス)ブラウザで描画し、実際に結果を目で見ること。この種のバグを捕まえられる検査は
これだけです — 実際、私はこの方法でいくつも捕まえました。テンプレートに移すまで見えなかった
通貨コンバーターもその1つです。
「自己完結」ではなかった
エクスポートは単一の自己完結型ファイルだと謳っていました。実際はそうでもありませんでした。
実行時にUIフレームワークを公開CDNからこっそり取得していたのです — コード内のURL断片から
組み立てられるため、CDNを普通にテキスト検索しても何も見つからず、ファイルは見た目には
オフライン対応でありながら、まったくそうではありませんでした。厳格なセキュリティポリシーを
持つウェブホストでは、その隠れた取得がブロックされ、アプリ全体が起動に失敗しました。
そうしたポリシーのない自分のノートPCでは動いた — つまり、本物のサーバーに当たるまで問題は
見えなかったのです。
解決策は、フレームワークをセルフホストする(外部への取得がないようアプリと一緒に
バンドルする)こと、そしてダウンロード用サンプルでは直接インライン化することで、全体が
本当に1つのファイルになり、インターネットなしでもディスクから開けるようにしました。この節
から実用的なことを1つ持ち帰るなら:「自分のマシンでは動く」と「本物のウェブサーバーの
セキュリティヘッダーの背後でも動く」は別の主張だということ — 後者をテストしましょう。
ゲート:見せかけ vs. 本物の鍵
最初のバージョンは、ページのJavaScript内のPINでサイトをゲートしていました。それは
セキュリティの見せかけです:旅程全体がページのソースに載っているので、開発者ツールを
開ける人なら誰でも、コードを一度も打ち込まずに読めてしまいます。ちょっと覗かれる程度から
サプライズパーティーを隠すには十分ですが、本当に大切なものには不十分です。
本物のバージョンは、ゲートをサーバーに移しました:PINを送信すると、サーバーがそれを
照合してセッションを開始し、そのときに初めて旅程を渡します。さらにIPごとの段階的
ロックアウトも備えています — 数回間違えるとそのアドレスは次第に長い時間タイムアウトされる
ので、6桁のコードを片っ端から連射することは誰にもできません。
限界についても正直に言います。ごまかすと痛い目を見るからです:レート制限しかない6桁の
PINは、リスクの低い家族の旅程には十分ですが、銀行並みではありません。機微なものをゲート
するなら、長いパスフレーズときちんとしたログインを使いましょう。鍵は、扉の向こうにある
ものに見合わせること。
AIアシスタントは短い綱で
楽しい部分:自分専用のタブに住む小さなアシスタント — Yoshi と名付けました。
プライベートな家族版では、旅行についての質問(「水族館の日のプランは?」「何を持っていけば
いい?」)に答え、スケジュールに小さくガードの効いた編集を加えられます。肝心なのは、
壊れやすいアプリではなく、あの安全なデータファイルを編集する点です — そして囲われて
います:追加や微調整はできても、日程は削除できず、フライトと予約はハードロックされていて、
どんなチャットメッセージも手を触れられません。
重要なのは、Yoshiがセルフホストのローカルモデル — 有料のクラウドAPIではなく、自前の
ハードウェアで動く小さなAI — で動き、ガード付きのリレー越しに到達する点です。これはそれ
自体が一大テーマ(ローカルホストのチャットボットを安全に公開ウェブへ載せること、そして
コンシューマーハードウェアでどれほど遅いか)なので、別記事にまとめました:
チャットボットを公開ウェブに置く — 自前のハードウェアで。
下のダウンロードサンプルについての注意: アシスタントのタブは、それがどこにあるかを
見てもらうために置いてありますが、サンプルでは意図的にオフにしてあります — どのAIにも
つながっておらず、返答しません。自分のコピーで動かすには、自分のローカルモデルに向ける
必要があります。これは意図的です:動いているAIにこっそり通信する公開ファイルを配るつもりは
ありません。
オフライン、インストール可能、海外でも安心
最後の仕上げは、旅先で実際に使う現実に向けたものでした:途切れがちなホテルのWi-Fi、機内で
のデータ通信なし、来たり来なかったりするローミング。そこでアプリはインストール可能な
PWAになりました — ホーム画面に追加すると本物のアプリのように開きます — さらに
オフラインで動作し、一度読み込めば端末から旅程全体を配信します。1つだけ気をつけた
ルール:旅程をキャッシュするのはアンロックに成功した後だけにして、オフラインキャッシュで
ゲートが回避されないようにしました。オンラインでは常にゲートを再確認し、オフラインでは
自分のスマホにすでにあるコピーを信頼します。家族の旅行計画には、それが正しい取引です。
(サービスワーカーには鋭い刃があります — 古いキャッシュ版が、修正を出したずっと後まで
古いページを配り続けることがあります。古いものを削除するだけでなく、常に更新したワーカーを
出すようにして、ブラウザが変更を確実に拾うようにする、と学びました。)
本番同然にテストする、実際に本番だから
家族が旅の最中にこれを使っていたので、私はどの変更もデプロイと同じように扱いました:
ビルドし、起動後にヘッドレスブラウザで実際のページを描画し、本物のタブやボタンを操作し、
そのうえで初めて出す。旅の途中でも、この方法で小さな修正をライブに投入しました — 当日を
ハイライトする「TODAY」バッジ(閲覧者のスマホではなく東京時間を基準にする)、天気の更新、
いくつかのレイアウトの粗 — どれも先に本物のブラウザで検証しました。起動時再構築の罠と
同じ教訓を、もう一度身につけたわけです:ファイルが存在することは、機能が動くことでは
ない。
試してみる — サンプルをダウンロード
下のダウンロードは、アプリの匿名化した公開サンプルです。名前、日付、住所、詳細はすべて
架空のデモ内容で、フィクションの7日間の東京プランです。index.html をどのブラウザでも
開いてください — インストール不要、サーバー不要、アカウント不要。オフラインで動作し、
インターネットに出るのはライブ天気カードと地図のリンクだけです。
正直な注意が2つ。1つ目、これは単一の約14MBファイルです。アプリ、フォント、画像をすべて
インライン化して依存ゼロで動くようにしているためで、その分初回の起動が遅いです。本番用
ビルドならこれらを分割・圧縮するでしょう。2つ目、前述のとおり、サンプルでは
AIアシスタントはオフです — タブは開き、その旨を伝えます。
自分で作ってみる
冒頭の「自分で実装してみたい方へ」ボックスはスローガンではありません — この記事は、その
ままAIアシスタントに渡せるように書かれています。レシピを凝縮すると:
- 計画をデータファイルに入れる。 日程、立ち寄り先、メモ、天気 — 唯一の信頼できる
情報源となる、1つの読みやすいファイル。 - そこからアプリを生成する。 決定的なビルドステップがデータをUIに変えるので、編集は
常にデータに対して行い、生成コードには決して行わない。 - デザインツールのエクスポートから始めるなら、その起動を尊重する。 アプリの
テンプレートに差し込み、委譲イベントを使い、取得するものはすべてセルフホストする。 - ゲートはページ内ではなくサーバーで。 セッション+レート制限付きロックアウトは、
JavaScriptのPINに毎回勝ります。 - 旅に出るならオフラインファーストにする — キャッシュは本物のアンロックの後だけ。
- ブラウザで、起動後に検証する。 「ファイルが存在する」ではなく。画面を。
ハマりどころまとめ
- エクスポートは起動時にDOMを作り直す — ページに差し込んだものはすべて消える。代わりに
アプリのテンプレートに差し込み、イベントはdocumentに委譲する。 - 「自己完結」とは限らない — エクスポートは実行時にフレームワークを取得しうる。セルフ
ホストし、ローカルだけでなく本物のサーバーのセキュリティヘッダーの背後でテストする。 - クライアント側のPINは見せかけ — 中身はページにある。ゲートはサーバーで。
- 6桁のPINはレート制限があるだけで、破られないわけではない — リスクが低いなら十分、
機微なものには不適。 - サービスワーカーは積極的にキャッシュする — 修正を配るなら更新したワーカーを出す。
古いファイルを削除しても登録は解除されない。 - 「ファイルが存在する」≠「機能が動く」 — 毎回、描画して目で見る。
ダウンロード
- サンプルの旅程アプリをダウンロード(zip) 10.3 MB
個人利用は無料です。役に立ったら、コーヒーをおごってもらえると嬉しいです。