
Cloudflare Workers + D1 + R2で「TOP3だけ」のランキングSNSをβ公開---
はじめに
好きなものを1位、2位、3位の順で共有するランキングSNS「RANKLY」を個人開発しています。
一般向けの記事では、なぜTOP3にしたのかを書きました。本記事では、β版を動かしている技術構成と、実装中に方針を変えた部分をまとめます。
- アプリ:
- LP:
- 一般向け記事:
note公開後にURLを記載
リポジトリは現在非公開です。そのため、コードは構造が分かる範囲に簡略化して掲載します。
現在の構成
主な構成は以下です。
| 領域 | 採用したもの |
|---|---|
| UI | React 19 / Next.js App Router |
| ビルド | vinext / Vite |
| 実行環境 | Cloudflare Workers |
| DB | Cloudflare D1 / Drizzle ORM |
| 画像 | Cloudflare R2 |
| 認証 | Better Auth / Google OAuth |
| 言語 | TypeScript |
静的アセットとWorkerを同じプロジェクトから配信し、ランキングやユーザー情報はD1、アップロード画像の実体はR2へ保存しています。
Next.js互換の構成をCloudflare Workersで動かすため、vinextを利用しています。現時点ではβ版なので、構成を分散させすぎず、一つのプロジェクトで追えることを優先しました。
「TOP3」をDBでも制約にする
中心となるテーブルは、ランキング本体と順位項目です。
画面では常に3枠として扱い、保存時にも順位と件数を検証します。
最初は一般的なランキングのように件数を可変にする案もありました。しかし、RANKLYでは「たくさん紹介する」より「どれを1位にしたか」を見せることが中心です。そこで、TOP3という制約をUIだけでなくデータ構造にも反映しました。
フィードは「1投稿を縦、順位を横」
操作は次の2方向に分けています。
- 縦方向: 次のランキングへ移動
- 横方向: 同じランキングの1位、2位、3位を移動
スマートフォンでは分かりやすい一方、PCではスマートフォン相当の表示領域全体が画面内に収まらず、下部操作が見切れる問題がありました。
PC版を別UIへ作り替えるのではなく、まずはスマートフォン相当のシェルをビューポート内へ収める調整を入れています。モバイル中心のサービスでも、SNSや技術記事からPCで開かれることは多いため、β公開前に確認しておいてよかった点でした。
AIには「順位を決めて公開」させない
ランキング作成には、自然文からTOP3の下書きを作る補助機能があります。
ここで大事にしたのは、言語モデルの回答だけで商品名、店舗、画像、リンクを確定しないことです。
処理は大きく3段階に分けています。
- 入力文からテーマ、カテゴリ、地域、希望条件を構造化する
- カテゴリに合う外部サービスから候補を探す
- 編集可能な下書きとして返し、公開はユーザーが行う
情報源も一律ではありません。
- 商品、ゲーム、書籍など: 楽天系APIを優先
- 日本国内の飲食店: Hot Pepperを優先
- 都市、観光地、宇宙など: 知識・画像系の情報源を利用
- 動画が適しているテーマ: YouTubeを候補にする
- 画像が不足した場合: 画像検索系サービスを補助的に利用
最初は「楽天、YouTube、その他」のような共通順序に寄せかけました。しかし、ラーメン店を商品検索するのは不自然ですし、旅行先を物販と同じ手順で探すのも違います。
最終的には、入力を検索計画へ変換し、テーマごとに情報源を選ぶ形にしました。
また、自動生成の結果は必ず下書きです。ユーザーがタイトル、順位、画像、リンクを確認してから公開します。
D1で実装したもの
D1にはランキング以外にも、次の情報を保存しています。
- Googleログイン用のユーザー、アカウント、セッション
- プロフィール、確認済み表示
- いいね、保存、コメント、フォロー
- 通報、ブロック、利用停止
- AI利用回数と処理結果
- 体験アンケートとプロダクトイベント
- 外部APIの短期キャッシュ
- 画像安全確認の結果
β版でも公開投稿を扱うため、通報や利用停止を後回しにしないようにしました。
画像は投稿時の確認に加え、定期処理でも確認します。自動判定で止める対象と、人が確認する対象を分け、管理画面から判断できるようにしています。
途中でやめたこと
フィード表示のたびにデモデータを作る
初期実装では、フィード取得時にINSERT OR IGNOREでデモデータを確認していました。
デモには便利ですが、通常の読み取りへ書き込みが混ざります。データが増えた後は不要な処理になるため、現在は読み取り処理から外し、必要なデータは一度だけ実行するSQLへ分離しました。
すべてを一つの巨大コンポーネントに足す
開発初期は、操作を早く確かめるためにアプリシェルへ実装を集めました。β版まで動かすには速かったのですが、現在はファイルがかなり大きくなっています。
今後は、作成、フィード、プロフィール、管理といった単位で段階的に分割する予定です。個人開発では「最初から完璧に分割する」より、「分割すべき境界が見えた時点で切る」方が進めやすいと感じています。
旧DBの移植
RANKLY以前に使っていたD1には、別スキーマのランキングが残っていました。
旧DBではthemesとproducts、現在はrankingsとranking_itemsです。そのため、DBファイルをそのまま差し替えることはできません。
今回は次のように移しました。
- 旧DBを読み取り、公開テーマと上位3項目を抽出
- 現行DBをエクスポートしてバックアップ
- 固定IDと
INSERT OR IGNOREを使った再実行可能なSQLを作成 - ローカルD1で適用確認
- 現行DBへ追加
- APIからタイトル、公開状態、3画像を再確認
明らかにテスト文言が残るデータは公開せず、下書きとして保存しました。
この手順は別のQiita記事で詳しくまとめる予定です。
現在地
現在はdev環境でβ公開しています。
まだ、実際の利用者が迷う場所、作りたいテーマ、共有したくなる見せ方を確認している段階です。技術的にも完成形ではありません。
ただ、ランキングの作成、公開、閲覧、共有、Googleログイン、通報・管理まで、一通り試せる状態にはなりました。
もし触っていただけたら、「何が良かったか」より「どこで迷ったか」を教えてもらえるとうれしいです。
