MF Blogs 便利ツール
夜空と三日月の下、地図のグリッド上に置かれたオレンジ色の位置ピンとエッジノードのネットワーク

記事

「今夜泊まれる宿」だけを探すTOMALを公開——Cloudflare Workers+React Router 8で作る当日ホテル検索

終電後や災害時に「いま近くで今夜泊まれる宿」を最速で見つけるTOMALを作りました。楽天トラベルAPI、二層KVキャッシュ、47都道府県×8言語のSEO設計、PWA、匿名の需要計測まで、設計判断をまとめます。

0:00 0:00

終電を逃した、台風で電車が止まった、出張先で急に泊まることになった。そういう夜に必要なのは「来月の旅行の比較」ではなく、「いま立っている場所から歩ける範囲で、今夜空いている部屋」だけです。その一点に絞った検索サービスTOMAL(トマル)を公開しました。筆者(MeeFa)が開発・運営しています。

大手OTAは半年先までの在庫と無数のプラン条件を扱う必要がありますが、TOMALは「今夜・現在地・空室」以外を切り捨てることで、検索から予約サイトへの遷移までを数タップに収めています。本稿では、その設計判断と技術スタック、公開後5週間で見えた数字を記録します。

「今夜」に絞ると、UIも技術も軽くなる

TOMALの画面は、現在地の許可、距離順に並んだ空室カード、ホテル詳細、予約サイトへの遷移、の4段だけです。カードには徒歩分数(直線距離を80m/分で換算)、今夜の最安価格、そして深夜チェックインの期限(「〜29:30 IN可」のような日本のホテル特有の表記)を出します。終電後の利用者が本当に知りたいのは、星の数より「何時までに着けば泊まれるか」だからです。

チェックイン日は常に「今日」、チェックアウトは「明日」、人数は1〜2名です。入力項目がほぼ存在しないので、フォームのバリデーションも状態管理も最小で済みます。「今夜」という制約は、UXの制約であると同時に、実装を軽くする制約でもありました。

技術スタック: React Router 8 framework mode on Cloudflare Workers

構成はpnpmワークスペースのモノレポで、フロントとバックエンドを1つのWorkerに同居させています。

  • apps/web: React Router 8(framework mode、SSR)+Vite+Tailwind CSS v4。Cloudflare Workersで配信し、/api/*はリソースルートとして同じWorkerが処理します
  • packages/core: ドメインモデル(Hotel、Market、Station、距離計算)。React・Workers・DBに依存しない純粋なTypeScriptです
  • packages/schemas / api-client: Zodスキーマと型付きクライアント。将来のモバイルクライアントがこの層だけを共有する想定です
  • Cloudflare KV: 検索結果の二層キャッシュ。D1: 市場スナップショットと匿名計測。Cron Triggers: 1日3回の市場データ収集

ホテルの空室データは楽天トラベルの公開APIから取得しています。座標+日付で当日空室を返せるAPIは、調べた限り日本ではこれがほぼ唯一で(Booking.comのDemand APIはマネージドパートナー限定、じゃらんは新規受付停止)、データ源の選択肢は最初から決まっていました。

レート制限と「鮮度」の両立——二層KVキャッシュ

楽天APIには秒間リクエスト数の上限があり、検索ページを開くたびに叩くと、少し人が来ただけで止まります。一方で当日の空室は刻々と変わるので、長くキャッシュすると嘘の情報を出すことになります。

落とし所は「保存は30分、読み出し時の許容鮮度は用途で変える」という二層構成にしました。現在地検索は5分以内のキャッシュだけを再利用し、エリアページやガイドの埋め込みウィジェットは30分まで許容します。同一キーへの同時リクエストはインフライトの1本にまとめ(コアレッシング)、429/5xxは1回だけリトライします。ホテル詳細は24時間、今夜のプラン一覧は5分と、データの変化速度ごとにTTLを分けています。

データ保存TTL読み出し時の許容鮮度
現在地検索の結果30分5分
エリアページ・埋め込みウィジェット30分30分
今夜のプラン一覧5分5分
ホテル詳細24時間24時間

すべてのページに「空室状況 21:05更新」のような取得時刻を表示しているのは、この設計の帰結です。鮮度をユーザーから隠さない方が、キャッシュを長めに取っても信頼を損なわないと考えています。

SEOは「ページ数」ではなく「実データ入りの薄くないページ」で

検索流入の受け皿として、47都道府県ページ、38の主要エリアページ、約250の駅ページを用意しました。ただし定型文だけのページを300枚並べると、Googleのスケールドコンテンツ抑制に引っかかります。そこで各ページに、そのエリアの今夜の空室数・最安価格・上位5軒をSSRで埋め込み、さらにライブ数値を使ったQ&A(「今から予約できますか?→現在57件、最安¥5,510〜」)を生成しています。ページごとに内容が本当に違う状態を作りました。

構造化データはBreadcrumbList / ItemList / FAQPage / Hotel / Article、OGP、hreflang、/llms.txt(AIアシスタント向けのサイト概要)まで揃えています。UIとガイド記事は8言語(日・英・簡体・繁体・韓・タイ・ベトナム・インドネシア)に対応し、/en/guides/...のようにロケール接頭辞のURLで分離しています。ホテル名やプランは楽天由来の日本語のままですが、訪日客が困る「25:00という時刻表記の意味」のような部分はガイドで補っています。訪日客向けにはHotelTonightとの比較記事も用意しました。

計測は「個人情報ゼロ」で成立させる

TOMALにはアカウントも会員登録もなく、位置情報はブラウザ内で処理して保存しません。そのうえで事業判断に必要な数字は欲しいので、計測を「日×時間帯×エリア×言語×種別」の匿名カウントだけに絞りました。現在地検索が実行されたら5km圏内の最寄りエリアに丸めて記録し、IP・UA・Cookieは一切持ちません。アクセス解析もCookieレスのCloudflare Web Analyticsだけにしました。

「新宿で22時以降に何人が今夜の宿を探したか」は分かるが「誰が」は分からない、という設計で、プライバシーポリシーの「個人情報を取得しない」を守ったまま需要データが貯まります。

PWA化——検索経由でなくても辿り着ける導線

Service Workerはナビゲーションをネットワーク優先にし、ページHTMLはキャッシュしません(空室情報の鮮度が最優先のため)。オフライン時だけ、外部アセット参照ゼロの自己完結ページを返し、そこに「今夜の候補」として保存したホテルの名前・住所・電話番号をlocalStorageから表示します。地下や圏外で「さっき候補に入れた宿に電話したい」という場面のための設計です。ホーム画面追加の案内ページとmanifestのショートカット(長押し→今夜探す)も入れています。

公開5週間の数字

正直に書くと、流入はまだほとんどありません。Search Consoleでは446ページが「検出 - インデックス未登録」のままで、新規ドメインのクロール優先度の問題に直面しています(現在、手動のインデックス登録リクエストとIndexNow経由の通知で対処中です)。一方で、届いた数十件の検索のうち62%が22時〜4時の深夜帯に発生しており、「終電後の需要」という仮説は今のところ否定されていません。市場側の観測では、新宿は毎晩50〜60件の当日空室があり、最安価格は¥3,000〜5,000台で推移していました。

  • Search Console: 446ページが「検出 - インデックス未登録」
  • 検索の62%が22時〜4時の深夜帯に発生
  • 新宿の当日空室は毎晩50〜60件、最安は¥3,000〜5,000台

まとめ: 制約を絞ると全部が軽くなる

「当日・現在地」に絞った宿泊検索は、米国ではHotelTonightがAirbnbに買収されるまでに成長した領域ですが、日本では専用サービスがほぼ存在しません。ビジネスホテル・カプセルホテルという日本特有の当日在庫と、25:00/29:00という深夜チェックイン表記の文化は、汎用OTAのUIとは相性が悪いのです。TOMALはそこに、Cloudflare Workersのほぼゼロの運用コストで「需要が本当にあるか」を測りにいく実験でもあります。技術的には目新しさより「制約を絞ると全部が軽くなる」ことの再確認でした。

サービスは無料で、アプリのインストールも登録も要りません。TOMAL — 今夜泊まれる宿を、現在地から探すから使えます。東京の主要エリアの今夜の空室状況は東京の今夜の空室状況(エリア別)にまとまっています。