はじめに
ある日、ふと思った。
「個人サイトって今の技術で作ったらどうなるんだろう」
最初はお遊びのつもりだったが、気づいたら1500行のスパゲッティコードと、十数個のイースターエッグが生えていた。しかもTypeScriptのビルドは通っている。
この記事では、Claude・GeminiといったAIと協力しながら平成個人サイトを令和技術で再現した過程と、実装の裏側を紹介する。
技術スタック
React + TypeScript(Vite)
ホスティング:Vercel(無料)
DB:Supabase(アクセスカウンター用)
ドメイン:Cloudflare Registrar($12.20/年)
外部UIライブラリ:なし
外部UIライブラリは一切使っていない。
Win98のベベルボタンも、タイトルバーの青グラデも、全部インラインスタイルで書いた。
const Win98Button = ({ children, onClick, style = {} }: {
children: ReactNode;
onClick?: () => void;
style?: React.CSSProperties
}) => (
<button
style={{
background: "#c0c0c0",
border: "2px solid",
borderColor: "#ffffff #808080 #808080 #ffffff",
padding: "4px 12px",
fontFamily: "'MS Sans Serif', Arial, sans-serif",
fontSize: "11px",
cursor: "pointer",
}}
onMouseDown={(e) => (e.currentTarget.style.borderColor = "#808080 #ffffff #ffffff #808080")}
onMouseUp={(e) => (e.currentTarget.style.borderColor = "#ffffff #808080 #808080 #ffffff")}
>
{children}
</button>
);
AI分業体制
このプロジェクトはほぼAIとの共同作業だ。
| 担当 | 役割 |
|---|---|
| Claude | 設計・レビュー・壁打ち |
| Gemini | アイデア発散・実装(Antigravity経由) |
| えなどり | 意思決定・$12/年を払う人 |
Geminiには「平成初期の個人サイトのあるあるを発散してくれ」と投げ続けた。
俺の青春は、ニコニコ動画だったので。
Claudeには実装の設計レビューと、ハマったときの壁打ち相手になってもらった。
インフラ構成
ドメイン(Cloudflare Registrar)
Cloudflare Registrarはドメインを原価で売ってくれる。ena-dri.dev が$12.20/年。お名前.comより安い。
DNSの設定は1点だけ注意が必要だった。
Vercelにカスタムドメインを設定する場合、
CloudflareのプロキシをOFF(DNS only)にしないと
「Proxy Detected」エラーになる。
オレンジの雲マークをグレーにする、これだけ。
Vercel × GitHub
masterブランチにpushすると自動デプロイされる。
ひとつハマりポイントがあった。gitのメールアドレスとVercelアカウントのメールが一致していないとデプロイがブロックされる。
# 確認
git config user.email
# GitHubのメールに合わせる
git config user.email "your@email.com"
# 空コミットで強制トリガー
git commit --allow-empty -m "redeploy"
git push
Supabase(アクセスカウンター)
アクセスカウンターを本物にするためにSupabaseを使った。
create table visits (
id bigint primary key,
count bigint default 0,
updated_at timestamp with time zone default now()
);
insert into visits (id, count) values (1, 0);
alter table enable row level security;
create policy "Allow public read" on visits for select using (true);
create policy "Allow public update" on visits for update using (true);
フロントから直接叩く構成なので、環境変数は.env.localに書いてVercelの環境変数にも登録する。
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=sb_publishable_xxxx
.gitignoreに.envが入っていることを必ず確認すること。
実装したイースターエッグ(一部)
詳細なトリガーはサイトで探してほしいが、実装の概要だけ。
深夜猫(0:00〜4:59限定)
const isNight = time.getHours() >= 0 && time.getHours() < 5;
// ...
{isNight && <NightCat />}
深夜だけタスクバー猫が現れる。GIFはGeminiで生成してEZGIFで背景透過した。
テーマ切り替え(Win98 / WinXP)
ThemeContextでグローバルにテーマを管理して、スタートボタンクリックで切り替わる。XPモードではBlissの壁紙(あの緑の丘)が出る。
デフラグスクリーンセーバー
一定時間操作がないと起動する。赤・青・黒のブロックが並び「1.22GBの断片を解消中...」と永遠に1%のまま進む。
コナミコマンドで🦀の雨
const KONAMI = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown',
'ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
↑↑↓↓←→←→BA で🦀が降り注ぐ。それだけ。
動くイルカ(クリッピー的存在)
画面右下を泳ぎ回る。吹き出しのセリフは平成ネットミームを100種類詰め込んでランダム表示。
TypeScriptでハマったこと
verbatimModuleSyntaxが有効な場合、通常のimportでReactNodeを読み込むとエラーになる。
// ❌ NG
import { useState, ReactNode } from "react";
// ✅ OK
import { useState } from "react";
import type { ReactNode } from "react";
1500行のスパゲッティについて
現在Home.tsxは1500行を超えている。
コンポーネント分割?リファクタリング?知らない。TypeScriptのビルドが通っている以上、動いている。これは当時の個人サイト管理人のメンタリティの再現でもある。
ソースコードを読んだ人だけへのイースターエッグもある。
「見た目は平成、中身は令和」
当時の本物の個人サイトはHTMLが崩壊していても「なんとなく表示される」だった。このサイトは1500行スパゲッティでもTypeScriptが型チェックして、ビルドが通った瞬間に完璧に動く。
誰もReactだとは思わない。DevToolsを開いてビルド済みJSのサイズを見て初めて「え?」となる。それが一番のイースターエッグかもしれない。
おわりに
$12/年で遊び放題だ。サブドメイン取って色々公開したいと思っている。
えなどり / Indie Developer
Zenn: https://zenn.dev/ena_dri