世界のランドマーク996体の3Dビューアと地図の旅ページを作り、公開スイッチで本番を404に留めた
世界のランドマーク996体の3Dビューアと地図の旅ページを作り、公開スイッチで本番を404に留めた
朝、Google Drive のフォルダの URL を、画像と一緒に Claude Code へ貼った。 中身は世界のランドマークの3Dモデルで、フォルダに GLB ファイルが並んでいる。 これをサイトで見られるようにするのが、この日の出発点だった。
1000体をどこに置くか
中を見させると、東京のフォルダに GLB(1体あたり200〜500KB)と viewer.html が入っていた。 サイトは静的配信なので、気になるのは Pages のファイル数の上限(2万)である。 Claude Code には、まずこの上限に対する現状を測らせた。
返ってきた提案は、置き場所を分ける形だった。 3Dモデルは R2 に置き、mdx-playground にはビューアのページを足すだけにする。 Pages に増えるファイルは数個で、Workers(Functions)の利用もゼロのまま保てる。
メモリが張り付いた
提案が返ってきたところで、メモリがかなり張り付いていることに気づいた。 余計なものを止めてほしいと頼み、/procs で走りっぱなしのプロセスを掃除させた。 止めたのは、前日の dev サーバー(2プロセス、約2.6GB)と、前日の DevTools MCP 10個(約2.5GB)である。 空き物理メモリは 1.8GB から 7.4GB に戻った。 その日の朝に立てた dev サーバーは止めずに残した。
996体と、保留の4体
置き場所は提案の形で進めることにした。 Drive 上の GLB を全件数えさせると1009本あり、公式の総合索引と突き合わせた結果、表示に使える GLB は996体だった。 残る4体は、索引で「配布対象外(保留中)」になっている。 作者側がまだ確定版ではないとして索引からリンクを外しているモデルで、Drive に残っているのは前の版だという。 いずれ追加されるはずなので、取り込みは後回しにした。
R2 の使用量も実測させた。 2,272ファイル、255MB で、無料枠(10GB)の約2.5%である。 容量の心配はなかった。
この段階で計画書を作らせ、Codex のレビューを2回通した。 致命的な指摘が2件出て、どちらも直してから着手した。
東京の65体で型を作る
最初は東京の65体だけで進めた。 取得と圧縮のスクリプトを書かせると、65体の合計は 22MB から 4.4MB に縮み、sha256 も一致した。
このセッションでは DevTools MCP がつながっていなかった。 そこで、リポジトリにある Playwright でスモークテストを書かせ、表示を確かめた。 65体すべてが読み込め、表示までの時間は中央値17ms、最大470ms だった。 ハイドレーション不一致の警告が2件出たが、原因を潰して消した。 スマホ幅(390px)でも横へのはみ出しはなかった。
画面を見て、注文を2つ出した。 一覧はカードを4列に並べる。 単体で表示するときは、もう少し寄る。 あわせて、地域のモデルを並べた街並みのページも作らせた。
寄りが足りなかったのは、モデルを囲む球が画面に収まる距離でカメラを止めていたからだった。 モデルを囲む箱が画面の枠に収まる最短の距離まで寄せる計算に変えさせると、モデルが画面の約9割を占めるようになった。
圧縮版だけを残すと決める
この時点で、重いファイルがどこにあるのか気になった。 聞くと、まだ R2 には何も上げておらず、すべてローカル(git 管理外のディレクトリ)にあって約28MB だという。
1000体に増えたらどうなるか。 東京65体の実測(約2割に縮む)を996体の合計約327MB に当てはめると、圧縮版だけなら約65MB に収まる見込みだった。 圧縮前の元データは Drive に残っている。 だから R2 にも圧縮版だけを上げればよい、と判断し、その方針を計画書に書き足させた。
996体へ広げる
Phase 2 では、取得スクリプトを「圧縮したら元の GLB を消す」既定に変えさせ、並列で取得させた。 996体すべてを取得と圧縮まで通せて、失敗は0件だった。 圧縮版は 65MB で、見込みどおりに収まった。 カードの絵も前もって作らせ、996枚で 9.5MB になった。
996体のスモークテストは、→ キーで順に送って 996/996 が通った。 引っかかったのは街並みのページである。 ヨーロッパの街並みだけが「読み込み中」のまま先へ進まなかった。 単独で開いても同じだったので本物の不具合と見て、橋の本数のあたりから原因を追わせた。 直したあと、7地域の街並みがすべてエラーなしで描けた。
コミットとアップロードで止まった箇所
「コミットしてからアップロードしてみて」と頼んだ。
機能コードのコミットが、Codex のコミットレビューで止まった。
指摘は3件。
中身を見て重要度を判定し直させると、2件は本物だった。
残る XFF の1件は、dev サーバーが [::1]:3000 でしか待ち受けていないので外から届かず、low と判定した。
本物の2件は、画面を離れたときに描画の後始末を確実に走らせる修正と、圧縮を一時ファイルに書いて成功したときだけ差し替える修正で直させた。
XFF の1件は理由を残してレビューを飛ばし、コミットを通した。
続くセキュリティレビューの指摘にも対応させ、Drive 由来の索引の ID とファイル名を、パスやコマンドに使う前に検査するようにした。
ここまででコミットは5件になった。
アップロード先には、3Dモデル専用のバケットを新しく作らせた。 r2.dev もカスタムドメインもつながない、非公開のバケットである。 画像用のバケットに同居させなかったのは、本番デプロイの画像同期がマニフェストに無いキーを消すからだ。
一括アップロードは、GLB 996本までは通ったのに、カードの絵の分でエラーが出た。 ログを読んでも、どの束が落ちたのかはっきりしない。 バケットの件数表示も集計が遅れて反映されるので、0件と出ても当てにならなかった。 そこで S3 API で中身を直接数えさせ、429 と 504 を待って送り直す形で走らせ直した。 圧縮版 GLB、カードの絵、索引を合わせた1,993件がすべて入り、抜き取った11件でバイト一致を確かめた。 公開の設定は何もない。
これで、モデルは R2 に、ページはリポジトリにそろった。 あとはデプロイすれば公開だろうか。
公開の手前で止める
「フェーズ3に進んでいいんじゃないですか」と伝えた。 Phase 3 のうち外に影響するのは2つで、バケットにカスタムドメインをつなぐこと(この時点でモデルが誰でも読めるようになる)と、本番デプロイである。 対外的な操作は実行直前に確認する、というルールに沿って、この2つの手前で止めさせた。 先に進めさせたのは、コードと設定ファイル、ローカルでの確認だけである。
その準備の中で入ったのが、公開スイッチ LANDMARKS_PUBLISHED である。
false の間は、ランドマークの3ページとも本番で404になり、noindex も付く。
配信元も切り替え式にして、dev ではローカル、本番では R2 の3Dモデル専用バケットを指すようにした。
世界地図でめぐる旅のページ
もう1つ作りたいものがあり、イメージの画像を貼って頼んだ。 地図の上で、ランドマークを順にめぐるページである。
世界地図は d3-geo、topojson-client、world-atlas で SVG に描かせた。 Natural Earth の地図データを同梱して自前で描くので、外部の地図タイルは使わない。 立ち寄り先は ID 順の線でつなぎ、今いる地点へ地図が寄る。 遠くへ移るときは、いったん引いてから寄る。 3Dモデルは左下のカードに重ね、← → キーで次の地へ進む。
位置は OpenStreetMap の Nominatim で、都市とランドマーク名から引かせた。 1秒に1件のペースで約20分かかる。 1回目で992体に位置が付き、残る4体は都市名の引き方を変えて引き直した。 最終的に996体すべてに位置が付き、名前で特定できたのが683体、都市の位置で代えたのが313体である。
途中で、地図の陸地がまったく描かれなくなった。
ページの不具合かと思ったら、dev サーバーのほうが .nuxt/dist/server/server.mjs を見失い、全ページが500を返していた。
dev サーバーが動いている最中に地図のライブラリを入れたのが原因らしい。
dev サーバーが復帰してから、スクリーンショットを撮り直した。
メモリは、朝に掃除したあとも余裕がなかった。 旅のページができたあと、アイドル中にメモリ不足で dev サーバーが止められ、ローカルのページが開けなくなっていた。 壊れたわけではないので、見るときに立て直せばよい。
「普通にデプロイすればいい」のか
最後に進捗を聞いた。 ローカルで見て確かめるところまでは終わっていて、残っているのは公開に関わる作業だけだった。 そこで、公開は mdx-playground を Cloudflare に普通にデプロイすればいいのか、と尋ねた。
答えは「それだけでは公開されない」だった。 公開スイッチが false なので3ページは本番で404のままだし、バケットにもまだドメインがつながっていない。 公開をうっかり起こさないための安全装置が、二重にかかっていたことになる。
公開するには、次の順で進める。 この手順を計画書の Phase 3 に書き直させた。
- バケットにカスタムドメインをつなぐ
- CORS を設定する
- 疎通を確認する
- ボット対策を入れる
- 公開スイッチを true にする
- デプロイする
- 本番で確認する
CORS の設定ファイルも用意させた。 許可するのは GET と HEAD だけで、まだバケットには適用していない。 次のセッションでこの手順をそのまま始められるように、引き継ぎのプロンプトをダウンロードフォルダのテキストファイルに書き出させて、この日の作業を終えた。
学び
- 公開スイッチを false にしておけば、デプロイしても本番は404のままになる。デプロイと公開を別の操作に分けておくと、公開の判断を最後まで手元に残せる
- 依存パッケージは dev サーバーを止めてから入れる。動いている最中に入れると、
.nuxtのビルド出力を見失って全ページが500になった - 件数の確認は集計値ではなく実物を数える。バケットの件数表示は反映が遅れるので、S3 API で中身を直接数えて1,993件を確かめた