──日付ID・型設計・Vercelデプロイまで──

本ページでは、Next.js と TypeScript を用いて「フロントエンドのみで完結する日替わり占いアプリ」 を実装する際の技術的なポイントをまとめています。
扱う主なテーマは次の4つです。
- 日付と入力条件から結果を固定する「決定論的ランダム」のロジック
- TypeScript による占いデータの型設計
- Next.js クライアントコンポーネントで完結させる構成
- 実装時に意識した設計上の工夫(タイムゾーン・ハッシュ・OGP など)
最後に、「寄り道おみくじ」というプロダクトの性質に合わせて、どのような設計判断をしたかも簡単に触れます。
日替わり占いで満たしたい挙動
今回の占いアプリでは、次のような挙動を満たすことを前提にしました。
- 同じ日 × 同じ星座 → 同じ結果を返す
- 日付が変わると → 結果が変わる可能性がある
- バックエンドや DB は用意せず、フロントエンドのみで完結させる
- 占いの中身(テキストやラッキーアイテム)は配列で管理し、後から増減しやすくする
Math.random() をそのまま使ってしまうと、ページを開き直すたびに結果が変わり、「同じ日なのに毎回違う結果になる」状態になります。
そのため本アプリでは、「今日」と「星座」から結果を計算して固定する方針にしています。
ここでは
「決定論的に計算されるが、見た目はランダムに振る舞うインデックス」
として実装しています。
TypeScriptによる占いデータの型設計
先に「何を扱うか」の型から固めておきます。
今回は星座占いを題材にしているため、最低限つぎのような要素を持たせています。
- 星座(12種類)
- 運勢レベル(大吉・中吉・小吉・凶などのイメージ)
- メッセージ本文
- ラッキーアイテム名
星座と運勢レベルの型
// domain/fortune.ts
export const CONSTELLATIONS = [
'おひつじ座',
'おうし座',
'ふたご座',
'かに座',
'しし座',
'おとめ座',
'てんびん座',
'さそり座',
'いて座',
'やぎ座',
'みずがめ座',
'うお座',
] as const;
export type Constellation = (typeof CONSTELLATIONS)[number];
export type FortuneLevel = 'great' | 'good' | 'neutral' | 'bad';as const によって、Constellation はリテラル型の共用体になります。
存在しない星座名を扱おうとすると、その時点で TypeScript が警告してくれます。
メッセージとラッキーアイテム
export type FortuneMessage = {
id: string;
level: FortuneLevel;
text: string;
};
export type LuckyItem = {
id: string;
name: string;
};
export const MESSAGES: FortuneMessage[] = [
{
id: 'msg_001',
level: 'good',
text:
'今日は、小さな「やらないこと」を一つ決めてみると、気分が軽くなりやすい日です。',
},
// 必要に応じて追加
];
export const LUCKY_ITEMS: LuckyItem[] = [
{ id: 'item_001', name: 'いつもと違う飲み物' },
{ id: 'item_002', name: '小さなメモ帳' },
// 必要に応じて追加
];このようにしておくことで、
levelに定義外の値を書くとコンパイルエラーになる- メッセージ構造が明示されるため、UI 側とのズレが起きにくくなる
- 将来的に JSON に切り出したときも、同じスキーマで検証しやすい
といったメリットがあります。
日付×星座からインデックスを生成するロジック
ここからが本題の「決定論的ランダム」です。
日替わりにするために、まず「今日」という条件を 日付ID に変換して扱います。
※日付IDは、乱数でいう seed(入力値)に近い役割です。以降は「日付ID」と呼びます。
日付IDを作る(ローカルタイム版)
// lib/seed.ts
const getDateSeedLocal = (date: Date): number => {
const y = date.getFullYear();
const m = date.getMonth() + 1; // 0始まりなので +1
const d = date.getDate();
// YYYYMMDD のような整数にする
return y * 10000 + m * 100 + d;
};この関数は「ユーザーのローカルタイム」に基づく日付をIDとして使います。
ユーザーごとにタイムゾーンが違ってもよい、という前提であればこれで十分です
日本時間固定で扱いたい場合(日付IDをJST基準にする)
// lib/seed.ts
const getJstDateSeed = (date: Date): number => {
const utcMs = date.getTime() + date.getTimezoneOffset() * 60_000;
const jst = new Date(utcMs + 9 * 60 * 60_000); // UTC+9
const y = jst.getFullYear();
const m = jst.getMonth() + 1;
const d = jst.getDate();
return y * 10000 + m * 100 + d;
};- ブラウザのタイムゾーン設定に関わらず、常に「日本時間の 0:00」を境に日付が変わる
- 日本国内向けのサービスであれば、このほうがユーザー体感とズレにくいです
日付IDを簡易ハッシュで散らす
日付IDをそのまま mod してもよいのですが、連番に近い値が続くと偏りが出やすくなります。
そこで、軽量なビット演算ベースの「簡易ハッシュ」を挟みます。
// lib/seed.ts
const hash = (value: number): number => {
let x = value >>> 0;
x ^= x << 13;
x ^= x >>> 17;
x ^= x << 5;
return x >>> 0;
};ここで使っているのは、擬似乱数生成器(PRNG)でよく使われるXorshift 系の簡略パターンです。暗号学的な安全性はありませんが、日替わりコンテンツ用途としては十分な散らばりが得られます。
日付ID+星座 → 配列インデックス
// lib/seed.ts
export const getDeterministicIndex = (
date: Date,
constellationIndex: number,
modulo: number,
offset = 0
): number => {
const dateId = getDateIdJst(date); // もしくは getDateIdLocal
const mixed = hash(dateId * 31 + (constellationIndex + offset) * 17);
return mixed % modulo;
};ポイントは次のとおりです。
31や17は素数で、連番を混ぜるときの定番パターンとして採用していますoffsetを変えることで、同じ日・同じ星座でも「メッセージ用」「アイテム用」で違う系列のインデックスを作れます
例として、
- メッセージ →
offset = 0 - ラッキーアイテム →
offset = 101
のようにずらすと、同じインデックスが両方に出るパターンを避けやすくなります。
Next.jsクライアントコンポーネントでの実装
アプリは SSR や外部 API を必要としないため、クライアントコンポーネント1つで完結させています。
UI実装例
// app/page.tsx
'use client';
import { useState } from 'react';
import {
CONSTELLATIONS,
Constellation,
MESSAGES,
LUCKY_ITEMS,
} from '@/domain/fortune';
import { getDeterministicIndex } from '@/lib/seed';
export default function FortunePage() {
const [selected, setSelected] = useState<Constellation | ''>('');
const [message, setMessage] = useState<string | null>(null);
const [item, setItem] = useState<string | null>(null);
const handleFortune = () => {
if (!selected) return;
const constellationIndex = CONSTELLATIONS.indexOf(selected);
if (constellationIndex < 0) return;
const today = new Date();
const msgIndex = getDeterministicIndex(
today,
constellationIndex,
MESSAGES.length,
0
);
const itemIndex = getDeterministicIndex(
today,
constellationIndex,
LUCKY_ITEMS.length,
101
);
setMessage(MESSAGES[msgIndex].text);
setItem(LUCKY_ITEMS[itemIndex].name);
};
return (
<main className="min-h-screen flex flex-col items-center justify-center px-4">
<h1 className="text-xl font-bold mb-4">寄り道おみくじ</h1>
<div className="mb-4">
<select
className="border rounded px-3 py-2"
value={selected}
onChange={(e) =>
setSelected(e.target.value as Constellation | '')
}
>
<option value="">星座を選んでください</option>
{CONSTELLATIONS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
<button
onClick={handleFortune}
className="px-4 py-2 border rounded"
>
占う
</button>
{message && item && (
<div className="mt-6 border rounded p-4 max-w-md w-full">
<h2 className="font-semibold mb-2">今日のメッセージ</h2>
<p className="mb-4 text-sm">{message}</p>
<p className="text-xs text-gray-600">
ラッキーアイテム:{item}
</p>
</div>
)}
</main>
);
}ここで意識しているのは次の2点です。
- 状態管理は
useStateのみで完結させる(小規模アプリなので十分) - 「日付×星座 → インデックス計算」は
lib/に切り出し、UI から見えるのは
「インデックスが返ってくる関数」と「配列」だけ にする
「寄り道おみくじ」ならではの設計ポイント
ここからは、単なる技術サンプルではなく、プロダクトとしての性質に合わせて調整した点を整理します。
当たり外れより「負荷の低い問いかけ」に寄せる
本アプリでは、以下のような方針でメッセージを設計しています。
- 「大当たり・大ハズレ」のような極端な結果は付けない
- 人格を断定する表現は避ける(「あなたは〜な人だ」など)
- 読んだときに「少し立ち止まる」くらいの温度感にする
技術的には、
- どのインデックスを引いても、一定以上の「ポジティブ要素」を満たすデータだけを配列に置く
という形で担保しています。
星座ごとの「重み」を付けない
星座ごとに出やすい運勢レベルやメッセージ群を変える設計も可能ですが、今回は実装していません。
- 「寄り道」の名のとおり、軽く使ってすぐ離脱してよいツールにしたい
- 星座ごとのバランス調整を始めると、設計と検証コストが一気に増える
という判断です。
拡張するなら、「星座×運勢レベルのテーブル」を用意して、前段でレベル帯を決める構成が考えられます。
よくある失敗パターンと注意点
Math.random()だけで決めると、リロードや再訪問で結果が変わります- 日付だけを入力にすると、同じ日で全ユーザー同じ結果になります(星座などの入力条件を必ず混ぜます)
- 配列要素数の増減で結果が変わることがあります(整合性が重要ならバージョン管理を検討します)
補足:Figma / GitHub / Vercel / OGP 周り
Figma生成コードの扱い方
Figma生成コードは「見た目のデザインベース」として必要な部分だけを採用し、使っていない ui/ 配下のファイルは削除しました。
ドメインロジック(型・日替わり計算)は domain/ と lib/ に集約し、UIと分離しています。
GitHubリポジトリと Vercel デプロイ
小規模アプリのため、ブランチは main のみに統一し、不要ファイルを削ってから Vercel に接続しています。
これでビルド~デプロイまでが自動化されます。
OGP 設定と「静的 or 動的」問題
今回は静的OGPで割り切っています。
// app/layout.tsx
export const metadata = {
title: '寄り道おみくじ',
description: '星座を選んで、今日のひと言メッセージを受け取れるミニアプリ。',
openGraph: {
title: '寄り道おみくじ',
description: '今日の星座とひと言メッセージ',
images: ['/og-yorimichi.jpg'],
},
};- note等ではこのOGPがそのまま使われることが多いです
- WordPress(テーマによっては)ブログカード側がOGPを生成することがあり、その場合はURLの貼り方やテーマ設定の影響を受けます
- 結果に応じてOGPを変えたい場合は、
@vercel/ogを使った動的OGP生成も選択肢になります
まとめ
本記事では、Next.js と TypeScript を用いて
- 日付×星座から「決定論的ランダム」で結果を決めるロジック
- 占いデータを型安全に管理する設計
- クライアントコンポーネント1ページで完結させる構成
- 「寄り道おみくじ」という軽量なツールに合わせた設計上の判断
を整理しました。
この考え方は占いに限らず、日替わりメッセージ/診断コンテンツなどにも流用できます。
ご案内
👇実装した【寄り道おみくじ】をお試しください。



コメント