ハコヅミを作る #26:過去のキャンプからキャンプ場を引用し、独自ドメインで開く(ステップ1 完了)

前回(#25)でキャンプの記録が整いました。今回は、ステップ1 の残りの2つです。

  • 過去のキャンプから、キャンプ場の情報を引用する(#87)
  • 本番を、独自ドメイン(hakozumi.jiru-labo.com)で開けるようにする(#80)

あわせて、本番リリースの PR で落ちた E2E の直し(#126)と、ステップ1 の締めくくりも書きます。


キャンプ場のマスタは、作らないことにした

同じキャンプ場に何度も行くと、キャンプを登録するたびに、名前・住所・電話番号・URL を入れ直すことになります。

最初の案は、キャンプ場を別のテーブル(マスタ)で管理し、キャンプの登録で選ぶ形でした(#86)。Issue まで作りましたが、作る前に考え直して、閉じています。

  • マスタを別に登録・手入れする手間がかかる。行くキャンプ場は限られていて、過去のキャンプがそのまま一覧の代わりになる
  • 将来、ほかの人も使えるようにしたとき(ステップ2)、誰がマスタを手入れするのか、という問題が出る。自分のキャンプから引くだけなら、起きない

代わりに、キャンプの登録・編集画面に「過去のキャンプ場から引用」の選択欄を置きました。

キャンプの登録画面の一部。「過去のキャンプ場から引用」の選択欄で「かわべりオートキャンプ場(架空)」を選ぶと、その下に引用される画像が出て、「保存すると、この画像をコピーして使います」と書かれている。下のキャンプ場の名前の欄には、選んだ名前が入っている

選ぶと、その場でキャンプ場の欄に値が入る。画像があれば、引用される画像も出る

候補の決め方

候補は、キャンプ場の名前が入った、自分の過去のキャンプです。

  • 同じ名前が複数あれば、最後に保存したものだけ(前後の空白を除いて、完全に同じ文字かで比べる。「ふもとっぱら」と「ふもとっぱらキャンプ場」は別)
  • 編集画面では、編集しているキャンプ自身は出さない
  • 名前順に並べる

この決め方は campsiteCandidates() に切り出して、ユニットテストしています。表記のゆれ(似た名前をまとめる)は、今回はやっていません。

画像は、引用ではなくコピー

キャンプ場の画像も引用します。ただし、引用元の画像を指すのではなく、コピーします。R2 の画像を、今のキャンプ用の新しいキーに複製します。

コピーにしたのは、引用元のキャンプを消したり、画像を差し替えたりしても、引用した側の画像が消えたり変わったりしないためです。E2E でも、引用元のキャンプを削除したあとに、引用した側の画像がまだ R2 にあることを確かめています。

引用した画像を使うのは、キャンプにまだ画像がなく、新しい画像も選んでいないときだけです。サーバーでは、引用元のキャンプが自分のものかを確かめてからコピーします。

選択欄に name を付けない

選択欄で選ぶと、ブラウザの JavaScript で、キャンプ場の名前などの欄に値を入れます。どのキャンプから引用したかは、隠しの欄(quoteFrom)で送ります。

選択欄そのものには name を付けていません。JavaScript が動かないときに、選んだだけで画像がコピーされ、名前などの欄は空のまま、という食い違いを起こさないためです。動かないときは何も送られず、今までどおり手で入れます。

長い名前で、画面全体が横にずれる

画面を撮って確かめると、スマホの幅で、画面全体が横にずれていました。

選択欄(<select>)の幅は、いちばん長い選択肢の文字に合わせて広がります。キャンプ場の名前は100文字まで入れられるので、長い名前が1つあると、選択欄が画面からはみ出します。部品 Select に max-w-full を付けて、親の幅を超えないようにしました。前から気になっていた、プランの選択欄のはみ出しも一緒に直りました。

「候補が1つもない」を、どう確かめるか

「キャンプ場の名前が入ったキャンプが1つもないときは、選択欄を出さない」という AC もありました。ところが、E2E の利用者は固定(自分1人)で、ほかのテストで作ったキャンプも D1 に残っています。名前のないキャンプしかない状態を、ふつうには作れません。

このテストでは、テストの間だけ、自分のキャンプのキャンプ場の名前を別のテーブルによけておき、終わったら(失敗しても)戻すようにしました。少し強引ですが、PR の「レビューしてほしい点」に書いて、見てもらっています。


本番を、独自ドメインで開く

本番は hakozumi.jiru-labo.workers.dev で動いていました。個人事業のドメインのサブドメイン hakozumi.jiru-labo.com で開けるようにして、事業のサービスの1つだと分かる URL にしました(#80)。

jiru-labo.com/service/hakozumi のようなパスにする案もありました。ただ、アプリの手直しが要り、ブログと Cookie などが同居するので、サブドメインにしています。

コードの変更は、設定の数行だけ

Cloudflare Workers では、Worker にカスタムドメインを付けられます。DNS のレコードと証明書は、Cloudflare が作ってくれます。

// wrangler.jsonc の env.production(本番だけ。開発環境は workers.dev のまま)
"routes": [{ "pattern": "hakozumi.jiru-labo.com", "custom_domain": true }],
// 本番の workers.dev の URL も、当面残す
"workers_dev": true,

アプリのコードは変えていません。画面の URL は、すべて相対で書いているためです。

大事なのは、デプロイの前の準備

コードより大事なのは、順番でした。ハコヅミの本番は、Cloudflare Access(ログインした人だけが開ける仕組み)で守っています(#2)。カスタムドメインを付けた直後から誰でも開ける状態にしないよう、先に Access に新しい URL を足してから、URL を付けるデプロイをすることにしました。

本番リリースの前に、ダッシュボードで4つを済ませています。手順は、画面のスクリーンショットを見せながら、Claude Code に1つずつ聞きました。

  1. jiru-labo.com のゾーンが、本番用のアカウントにあり、有効(Active)なこと。カスタムドメインは、Worker と同じアカウントのゾーンにしか付けられない
  2. Access のアプリケーションの「宛先」に、パブリック ホスト名 hakozumi.jiru-labo.com を足す。今の workers.dev の宛先は残す
  3. 本番のデプロイに使う API トークンに、ゾーンの「Workers Routes:Edit」の権限があること(カスタムドメインを付けるのに要る)
  4. hakozumi.jiru-labo.com の DNS のレコードが、まだないこと(あると付けられない)

2 では、どこを押せばよいか迷いました。Access のアプリケーションには、Worker の画面から作った「Workers」の宛先しかなく、「+ パブリック ホスト名を追加」から足すのが正解でした。

デプロイのあとに、ログインを求められるかを確かめる

本番リリースのあと、デプロイのログに hakozumi.jiru-labo.com (custom domain) が出ていることを確かめました。ログインしていない状態で開くと、Cloudflare Access のログイン画面に移ります(HTTP の 302)。workers.dev の URL も同じです。


リリースの PR で落ちた E2E

この2つの前に出した本番リリースの PR で、E2E が1件だけ落ちました。今回の変更とは関係のない、ギアの一覧のカード表示のテストです。

await tab(page, '欲しい').click();
await viewLink(page, 'リスト').click(); // ← 画面が移り終わる前に押している
await expect(page).toHaveURL('/gears?status=wanted');

「廃棄済」の画面で「欲しい」のタブを押し、すぐ「リスト」を押しています。CI で画面の切り替えが少し遅いと、まだ残っている「廃棄済」の画面の「リスト」を押してしまい、/gears?status=disposed に移って落ちます。

リリースは、落ちたものだけ流し直して通しました。そのうえで、テストの直しを Issue にしています(#126)。「欲しい」を押したあと、URL が変わるのを待ってから「リスト」を押すように、1行足しました。

await tab(page, '欲しい').click();
await expect(page).toHaveURL('/gears?status=wanted&view=card'); // 移り終わるのを待つ
await viewLink(page, 'リスト').click();

直したテストは、手元で20回続けて流して、落ちないことを確かめています。E2E 全体で同じ形(画面が移るリンクを、待たずに続けて押す)を探しましたが、ここ1か所だけでした。

CI で落ちたテストを自動でもう1回流す設定(Playwright の retries)もありますが、入れていません。たまに落ちるテストが隠れてしまうからです。#6でも、不安定な E2E は原因を1つずつ直してきました。


ステップ1 を締めくくる

MVPのあと、「自分用の機能を充実させる」ステップ1 に入りました。ここまでで、GitHub のマイルストーン「ステップ1」の Issue は 38 件になりました。

  • ギアの画像・おすすめ度・欲しいものリスト、一覧のカード表示
  • キャンプの予定と実績・キャンプ場・画像、おすすめ度と感想、キャンプ場の引用
  • 消耗品
  • タイトルバナー、フォント、見出し、ヘッダーとフッターなどの見た目
  • 独自ドメイン

Notion に書いた「ステップ1」の項目は、すべて済みました。ここでマイルストーンを閉じ、次からの Issue は「ステップ2」に入れます。

ついでに、/issue のスキルを直しました(#131)。スキルには、Issue を作るコマンドの例が --milestone MVP のまま残っていました(実際には、Claude Code がそのつどステップ1 に読み替えていました)。段階が変わるたびに直さなくて済むよう、「開いているマイルストーンに入れる」に書き換えています。

ステップ2 は「複数ユーザーでの共有」です。ユーザーごとのログイン、キャンプ履歴やおすすめ情報の共有など、ここからは自分以外の人が使うことを考えることになります。


おわりに

  • マスタを作る前に、「今あるデータを一覧の代わりにできないか」を考える
  • 引用した画像は、指すのではなくコピーする。元を消しても残る
  • <select> は、長い選択肢があると画面からはみ出す。max-w-full を付けておく
  • 独自ドメインは、設定よりも「先に守ってから付ける」順番が大事
  • たまに落ちるテストは、流し直して終わりにせず、原因を直す
← トップページに戻る