/tech-stack
telosh.xyz の実装メモ
このページは、ポートフォリオサイトそのものを題材にした技術メモです。採用したスタックの羅列にとどめず、どこで何を分離し、どんなトレードオフを取ったかを、同じような構成を検討するエンジニア向けに書き残しています。
はじめに
公開面はシンプルなマルチページ構成です。一方で、問い合わせの永続化・スパム対策・非公開の管理 UI まで含めると、小さなプロダクトに近い責務になります。そのため「見た目の実験場」だけでなく、 本番で破綻しにくいサーバー境界と状態管理を意識して組み立てています。
文章は、コードリーディングの手がかりになるよう、ディレクトリ名やファイル名を適宜示します。 リポジトリ全体の設計思想は docs/PROJECT_DESIGN.md や .cursor/spec にも分散しているので、合わせて参照すると輪郭が掴みやすいです。
アーキテクチャの骨格
フレームワークは Next.js 16(App Router)です。ルートの app/layout.tsx では Nunito と JetBrains Mono を CSS 変数経由で読み込み、 全体を MotionProvider で包んでいます。スクロールに追従する細いプログレスバーは ScrollProgress として切り出し、レイアウトに常駐させています。
計測は Vercel Analytics と Speed Insights をレイアウト末尾に配置し、ページ単位の計測を追加実装なしで賄っています。
ディレクトリ構成の俯瞰
次のツリーは、コードリーディングの入口になるよう、ルートと責務の対応がわかる粒度に絞っています。Next.js の慣習どおり、ページ専用の UI は app/…/_components/ に閉じ、複数ページから使う部品だけ components/ に上げる分離です。
app/actions/ はルートをまたぐ Server Action の置き場、lib/ は認証・DB・レート制限などサーバーと共有したい処理、stores/ はブラウザに閉じたクライアント状態、という層になっています。
ディレクトリの見た目は repo-folder-tree.tsx の専用コンポーネントで組み立てています。箱罫のテキストではなく、ネストしたリストと 1px の左ボーダー、固定の行高で階層を表すので、フォントの行送りやグリフ幅で縦線が途切れて見える問題を避けられます。日本語の説明は引き続き下の「ディレクトリのメモ」にまとめています。
- app/
- layout.tsx
- page.tsx
- globals.css
- actions/
- contact.ts
- about/
- page.tsx
- _components/
- projects/
- page.tsx
- _components/
- contact/
- page.tsx
- _components/
- _hooks/
- _lib/
- tech-stack/
- page.tsx
- _components/
- admin/
- layout.tsx
- actions.ts
- _components/
- contact-inbox/
- robots.ts
- sitemap.ts
- manifest.ts
- components/
- header.tsx
- footer.tsx
- scroll-progress.tsx
- sections/
- hero-section/
- mochifuwa/
- ui/
- providers/
- lib/
- db/
- index.ts
- schema.ts
- validations/
- admin-auth.ts
- require-admin.ts
- rate-limit.ts
- logger.ts
- animations.ts
- utils.ts
- stores/
- contact-form.ts
- drizzle.config.ts
- public/
ディレクトリのメモ
- app/
- App Router。ルートごとに page と、ページ専用の _components / _hooks / _lib。
- app/actions/
- ルートをまたぐ Server Action(問い合わせ送信など)。
- components/
- 複数ページから参照する UI(ヘッダー、ヒーローセクション、モチフワ部品)。
- lib/
- DB・認証・レート制限・バリデーションなど、サーバーと共有するロジック。
- stores/
- Zustand。フォーム下書きの persist などブラウザ内の状態。
- drizzle.config.ts
- Drizzle Kit の設定。マイグレーション出力は drizzle/(生成物、ツリーでは省略)。
- public/
- 静的アセット。
next.config や tsconfig)、docs/、.cursor/spec などはツリーから省略しています。ルートごとの実装
トップ(/)は HeroSection、TechStackSection、ProjectsSection、ContactCtaSection を縦に積んだランディングです。ヒーロー周りはクライアントコンポーネントですが、 ページ本体 app/page.tsx はサーバーコンポーネントのままです。
About / Projects / Contact は共通パターンで、Header と GradientOrbs(背景の装飾オーブ)を挟んだうえで、メインの体験をクライアントのセクションコンポーネントに任せています。 オーブは CSS アニメーション中心で、メインスレッドへの負荷を抑える意図があります。
管理コンソール(/admin)は公開ナビからは辿らせず、環境変数のシークレットでゲートする別世界として切り離しています。robots.txt では /admin/ を disallow しており、検索結果に載せない前提です。
ヒーローと 3D まわりの現状
依存には Three.js と @react-three/fiber が入っています。 リポジトリには hero-scroll-scene.tsx や hero-scene-model.tsx のように、 Canvas 内のシーンを組むための土台もあります。
ただし、現時点でトップにマウントされている HeroVisual3d は、プレースホルダー(静止画像と説明テキスト)です。WebGL シーンを常時載せるより先に、レイアウトとタイポの骨格を固める段階だと割り切っています。 差し替えはコンポーネント単位で完結するよう、ヒーロー右カラムだけを独立させてあります。
インタラクションと「モチフワ」
アニメーションは Motion(旧 Framer Motion)を使用し、 共通のスプリング設定を lib/animations.ts に集約しています。mochifuwaSpring や staggerContainer / staggerItem など、 触れたときに少し弾むような動きを、数値のばらまきではなくプリセットとして再利用できるようにしています。
ビジュアル面では、パステル調・大きな角丸・柔らかい影といったトーンを「モチフワ」と呼んでいます。components/mochifuwa/ には GradientOrbs や SquishButton など、 ブランド用の小さな部品を置き、ページセクションからはそこだけを import する形にしています。
お問い合わせ:フォームとサーバーの境界
送信処理は Server Actions(app/actions/contact.ts の submitContactForm)に集約しています。 クライアント側では React 19 の useActionState で送信中状態とサーバーからのメッセージを扱い、 フォームは従来どおり FormData を渡す形です。Next.js が Server Actions の CSRF 対策を肩代わりする前提で設計しています。
入力の正しさは Zod(lib/validations/contact.ts)でサーバー側のみ検証します。クライアントだけで完結させないことで、 改ざんや直接 POST に対しても同じルールが効くようにしています。
ユーザー体験としては、下書きの自動保存を zustand の persist ミドルウェアで localStorage に載せています(stores/contact-form.ts)。離脱やリロードに強くする一方、送信成功時にはストアをクリアして重複送信しやすい状態を残さないようにしています。
ハイドレーション直後のちらつきを抑えるため、下書き復元まわりは useReducer で状態をまとめ、 メール入力には補助用のカスタムフック(use-email-suggestion)を噛ませるなど、 フォーム専用の小さな状態機械がクライアント側にあります。
データベースと管理コンソール
永続層は Neon 上の PostgreSQL を想定し、Drizzle ORM でスキーマとクエリを記述しています。 問い合わせテーブルへの保存は上記 Server Action のバックグラウンド経路です。
管理側の認証はフルブローのユーザー管理ではなく、環境変数 CONTACT_INBOX_SECRET と照合する 共有シークレット方式です。 ログイン成功時には lib/admin-auth.ts で HMAC-SHA256 により導いた値を httpOnly Cookie に保存し、以降のリクエストではその値を timingSafeEqual で定時間比較します。Cookie にマスターシークレットそのものは載せません。
UI プリミティブと品質ツール
フォームの件名などで Radix UI の Select を使い、 キーボード操作やフォーカス管理をブラウザ標準より丁寧に扱えるようにしています(components/ui/select.tsx)。アイコンは lucide-react に統一し、 意味のある装飾だけに絞っています。
Lint / Format は Biome に一本化し、 Git フックには husky を使っています。型チェックは tsc --noEmit と組み合わせ、 CI やローカルで同じコマンドが通るようにしてあります。