Problem
個人開発は、何を作るか決める段階で止まりがちだった
個人開発でいちばん時間がかかるのは、実装ではなく 「何を作るか」を決める段階 だった。0 から 1 を立ち上げるのが苦手で、テーマが決まらないと手が動かない。決められない こと自体が、毎回いちばんの足止めになっていた。
そこで、自分で考えて決めるのをやめて、ランダムに引く道具 を作った。「ジャンル × アプリ種類」をボタンで一つ出し、出た組み合わせでそのまま作り始める。お題の良し悪しは考えず、まず手を動かすことを優先した。
Approach
スロットの "止まる瞬間" を中心に組む
出力形式を 「hogehoge × fugafuga」(例: サッカー × TODO)の 2 要素に固定し、左 = ジャンル / 右 = アプリ種類のスロットとして組んだ。結果のテキストより、左が止まり、続いて右が止まる という二段階の止まり方そのものを設計の中心に置いている。
スロットのフェーズは useSlot のステートマシン (idle / spinning / left-stopped / done) に閉じ込め、タイマーは useRef で持って useEffect のクリーンアップで確実に解放。アニメーションは DOM を直接触らず、Tailwind ユーティリティと CSS keyframes (animate-slot-blur / animate-bounce-in) だけで表現した。ロジックはフックへ、コンポーネントは presentational に寄せる方針。
状態は最小限に振り切る。履歴と項目カスタマイズは lib/storage.ts 経由の localStorage に集約し、失敗はそこで握りつぶして呼び出し側を try/catch から解放した。デザイントークンは Tailwind v4 の CSS-first 構成に従い、src/index.css の @theme ブロックに --color-* / --font-* として定義(tailwind.config.ts は使わない)。
シェアは静的サイトの弱点を埋める一手。結果を Cloudflare Pages Functions + workers-og(satori + resvg-wasm)でその場の動的 OGP 画像にし、X へテーマ名+画像付きで投稿できるようにした。サーバーを持たずにエッジだけで OGP を焼くこの構成が、技術的にいちばん遊んだ部分。
スロット抽選
ボタン or Space / Enter でスロットが回り、左 → 右の順に止まって「ジャンル × アプリ種類」が確定する。キーボード操作だけで完結。
ローカル履歴
引いた組み合わせを localStorage に蓄積。過去のお題を遡れるので、ボツ案の再利用や被りチェックに使える。
項目カスタマイズ
左右の候補リストを自分の好みで編集。テーマの縛りを自分のルールに寄せられる。下書き状態はこの画面だけがローカルに持つ。
動的 OGP シェア
シェアボタンで、抽選結果を焼き込んだ OGP 画像付きの X 投稿を生成。/og?left=…&right=… をエッジで都度レンダリングする。
インストール可能な PWA
Workbox の Service Worker でアプリシェルをキャッシュ。ホーム画面に追加すればオフラインでも起動する。
Result
サーバーを持たないまま、シェアまで実装した
theme-slot.c12o.net で公開中。スロット・履歴・カスタマイズ・OGP シェア・PWA までを、バックエンドを持たない静的サイト+エッジ Functions の構成だけで実装した。Cloudflare Pages に載せきっているので運用コストは実質ゼロ。
プロトタイプは 1 枚の HTML (theme-generator.html) から始め、そこから Vite + React + Tailwind v4 の本実装へ作り直した。「動くものを最速で出してから、壊れにくい形に組み直す」という個人開発の定石を、小さなスコープでひと通りなぞれた題材になっている。
Learnings
作って分かったこと
"間" を演出に変えると、単純な乱数が体験になる
左 → 右の二段階で止める表示を入れるだけで、同じ Math.random() でも結果を待つ時間そのものが体験になる。プロダクトの面白さは、ロジックの精度より「結果が出るまでの時間の設計」で決まることがある。
タイマー系の状態はステートマシンに閉じ込めると壊れない
回転 → 左停止 → 右停止のフェーズを useSlot の有限状態に押し込み、タイマーは useRef + クリーンアップで管理。連打やアンマウント時の取りこぼしが、状態を一箇所に集約した時点で消えた。
Tailwind v4 の CSS-first は設定ファイルを 1 枚消す
デザイントークンを tailwind.config.ts ではなく @theme ブロックに置くことで、設定の置き場所が CSS に一本化された。トークンとユーティリティが同じ言語に揃うと、デザインの調整が速い。
静的サイトの "シェアできない問題" はエッジで解ける
SPA は OGP が静的になりがちで、SNS シェアと相性が悪い。Pages Functions + workers-og で結果ごとに画像を焼くことで、サーバーを持たないまま「結果が見えるシェア」を実現できた。
localStorage アクセスは 1 ファイルに寄せると呼び出し側が軽くなる
loadJSON / saveJSON が失敗を握りつぶす lib/storage.ts を通すルールにしたら、履歴・カスタマイズの各フックから try/catch が消えた。永続化の例外処理は、境界の 1 箇所で吸収するのが結局いちばん読みやすい。