パーソナルポートフォリオサイト
Next.js 16、React 19、TypeScriptで構築したモダンでコンテンツ重視のポートフォリオ。AI支援開発でゼロから構築。
何年もの間、個人サイトを作ろうと思っていました。ドメインは埃をかぶったまま。アイデアはメモアプリから出ることはありませんでした。
そして、計画をやめて作り始めました。2日後、このサイトは公開されました。
何が変わったか
以前は、サイトを公開するには何週間もの作業が必要だと思っていました。フォントの選択、レイアウトのデバッグ、しっくりこないコピーの作成。今回は違うアプローチを取りました:AIと一緒に作り、素早くリリースし、後から改善する。
結果:48時間で51コミット。音声ナレーション付きのブログ。実際に機能するコンタクトフォーム。拡張可能なデザインシステム。完璧ではないが、リアル。
驚いたのはスピードではありませんでした。作業の感覚でした。検索が減り、構築が増えた。迷いが減り、リリースが増えた。ツールが面倒な部分を処理してくれたので、重要なことに集中できました。
このページは全ストーリーです。どのように組み立てられたか、内部の仕組み、そして次回何を変えるか。

技術スタック
| Category | Technology | Version | Purpose |
|---|---|---|---|
| Framework | Next.js (App Router) | 16.0.10 | Server components, routing, SSR |
| UI Library | React | 19.2.3 | Component architecture |
| Language | TypeScript | 5.x | Type safety |
| Styling | Tailwind CSS | 4.x | Utility-first styling |
| Components | shadcn/ui | New York | Pre-built accessible components |
| Content | next-mdx-remote | 5.0.0 | MDX rendering with frontmatter |
| Parsing | gray-matter | 4.0.3 | Frontmatter extraction |
| Icons | Lucide React | 0.561.0 | SVG icon library |
| SendGrid | 8.1.6 | Contact form delivery | |
| Analytics | Google Analytics 4 | — | Traffic and behavior tracking |
| Audio | ElevenLabs Audio Native | — | Text-to-speech for blog posts |

アーキテクチャ
アプリケーションはNext.js App Routerの規約に従い、ページ、コンポーネント、コンテンツ、ユーティリティを明確に分離しています。
ftchvs_26/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout with fonts and nav
│ ├── page.tsx # Home page
│ ├── globals.css # Global styles and design tokens
│ ├── about/ # About page
│ ├── blog/ # Blog listing and [slug] pages
│ ├── projects/ # Projects listing and [slug] pages
│ ├── contact/ # Contact form with API route
│ └── resume/ # Resume page
├── components/ # React components
│ ├── nav.tsx # Navigation with mobile sheet
│ ├── theme-toggle.tsx # Dark/light mode toggle
│ ├── tts-player.tsx # ElevenLabs audio player
│ ├── mdx-content.tsx # MDX renderer
│ └── ui/ # shadcn/ui components
├── content/ # MDX content files
│ ├── blog/ # Blog posts
│ ├── pages/ # Static pages (about)
│ └── projects/ # Project case studies
├── lib/ # Utilities
│ ├── content.ts # Content loading and parsing
│ ├── constants.ts # Site configuration
│ ├── schema.tsx # JSON-LD structured data
│ └── utils.ts # Helper functions
└── public/ # Static assets
データフロー
コンテンツはMDXファイルからlib/content.tsを経由してページに流れます:
- MDXファイルがYAMLフロントマター付きでコンテンツを保存
- gray-matterがフロントマターを型付きオブジェクトに解析
- next-mdx-remoteがMDXをReactレンダリング用にシリアライズ
- ページコンポーネントがリクエスト時にコンテンツを取得
- MDXコンポーネントが最終的なHTMLをレンダリング

タイポグラフィデザインシステム
すべてのページで一貫したスタイリングを実現するため、トークンベースのタイポグラフィシステムを構築しました。各トークンは特定のTailwindクラスにマッピングされています。
| Token | CSS Class | Styles | Usage |
|---|---|---|---|
| Page Title | .text-page-title | text-xl font-medium mb-6 | H1 headings |
| Section Title | .text-section-title | text-lg font-medium mt-8 mb-4 | H2 headings |
| Subsection | .text-subsection-title | text-base font-medium mt-6 mb-3 | H3 headings |
| Body | .text-body | text-[15px] leading-relaxed text-foreground/90 | Paragraphs |
| Body Container | .text-body-container | space-y-4 text-[15px] leading-relaxed | Multiple paragraphs |
| Meta | .text-meta | text-[14px] text-muted-foreground | Dates, tags, labels |
| Link | .text-link | underline underline-offset-2 hover:text-foreground | Inline links |
| List Item | .text-list-item | text-sm text-foreground/80 | Card and list text |
フォントスタック
- Geist Sans — 本文とUIのプライマリ書体
- Geist Mono — コードブロックとターミナルロゴ
両フォントは最適なパフォーマンスのためnext/font/google経由でdisplay: swapで読み込まれます。
カラーシステム
色は知覚的に均一な調整のためにOKLCH色空間を使用しています。システムには以下が含まれます:
- ライトモード — 控えめなアクセントを持つニュートラルグレースケール
- ダークモード — ハイコントラストテキストを持つ深い背景
- セマンティックカラー — Primary、Secondary、Muted、Accent、Destructive、Success
主な機能
ダークモード
テーマ設定はlocalStorageに保存され、初回訪問時はシステム設定を尊重します。<head>内のブロッキングスクリプトが間違ったテーマのフラッシュを防ぎます。
<script dangerouslySetInnerHTML={{
__html: `(function(){
var t = localStorage.getItem('theme') ||
(matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light');
if (t === 'dark') document.documentElement.classList.add('dark')
})();`,
}} />
テキスト読み上げ
プレーヤーはハイドレーションの問題を避けるため動的に読み込まれます:
const TTSPlayer = dynamic(
() => import('@/components/tts-player').then((mod) => mod.TTSPlayer),
{ ssr: false }
);
ElevenLabsでボイスクローンを作成する
オーディオプレーヤー用の独自のボイスクローンを作成するには:
- ElevenLabsにサインアップまたはログイン
- ダッシュボードのVoicesセクションに移動(左サイドバー)
- Add a new voiceをクリック
- Professional Voice Clone(最高品質推奨)またはInstant Voice Clone(より高速、必要な音声が少ない)を選択
- 音声サンプルをアップロード:
- Professional Voice Clone: 最低1時間の高品質音声(最良の結果には2-3時間が理想的)
- Instant Voice Clone: 最低30秒のクリーンな音声
- 長い録音は〜30分のサンプルに分割してアップロードを容易に
- 背景ノイズ、エコー、不要な音のないクリーンで高品質な録音を使用
- オプションでRecord yourselfを使用してインターフェースで直接録音
- ボイスクローンに名前とラベルを付ける
- 声をクローンする権利と同意があることを確認
- Save voiceをクリックしてモデルをトレーニング
- トレーニング完了後、音声設定からVoice IDをコピー
lib/constants.tsのELEVENLABS_VOICE_IDを更新してプロジェクトに設定
ボイスサンプル
クローンした声のサンプルはこちら:
音声サンプルを生成するには、ElevenLabs Text to Speech playgroundを使用できます。クローンした声を選択し、テキストを入力し、Generateをクリックして、ファイルをpublic/audio/ディレクトリにvoice-sample.mp3として保存してください。
ブログの音声ナレーション(ローカルのKokoro)
アップデート — 2026年4月。 ブログ記事とAboutページは、Kokoro-82Mを使うようになりました。Apache 2.0ライセンスのTTSモデル(パラメータ82M、ボイスaf_heart)で、kokoro-onnx経由でCPU上でローカルに動作します。生成は決定論的で無料、外部APIにも依存しません。上のボイスサンプルはオリジナルのElevenLabsでクローンした声のまま残しています — 移行したのはブログのナレーションだけです。
初期設定は一度だけ:npm run setup:kokoroがgitignoreされた.kokoro/ディレクトリにPython venvとONNXモデル(約340 MB)を作成します。その後npm run generate-audioで英語コンテンツすべてをナレーションし、public/audio/manifest.jsonを記事ごとのハッシュで更新します。Apple M4 Maxではリアルタイムの約5倍の速度で動き、3分の記事が約30秒で生成されます。
コンタクトフォーム
コンタクトフォームはSendGridを使用してメール配信を行います:
- クライアントサイドバリデーション
/contact/apiのサーバーサイドAPIルート- 直接返信用のReply-toヘッダー
- ユーザーフレンドリーなメッセージでのエラーハンドリング
SEO
すべてのページに含まれるもの:
- Metadata — タイトル、説明、キーワード
- Open Graph — ソーシャル共有用の画像とテキスト
- Twitter Cards — Twitter専用フォーマット
- JSON-LD — Person、Website、Article、Breadcrumb用の構造化データ
- Sitemap —
/sitemap.xmlに自動生成 - Robots — 検索エンジンのクロールルール

構築プロセス
フェーズ1: プロジェクトセットアップ
App Routerテンプレートを使用してcreate-next-appで開始。TypeScript、Tailwind CSS 4を追加し、shadcn/uiをNew Yorkスタイルバリアントで設定。
npx create-next-app@latest ftchvs_26 --typescript --tailwind --app
npx shadcn@latest init
フェーズ2: コアレイアウト
Geistフォントとレスポンシブナビゲーションでルートレイアウトを構築。ナビゲーションにはデスクトップメニューとshadcn/uiのモバイルシート(スライドアウトドロワー)が含まれます。
ターミナルロゴ(>_)はライトモードとダークモードに適応するシマーアニメーションを使用。
フェーズ3: コンテンツシステム
MDX読み込みを処理するlib/content.tsを作成:
getAllPosts()— 日付順にソートされた全ブログ記事を取得getPostBySlug()— シリアライズされたMDXで単一記事を取得getAllProjects()— 全プロジェクトを取得getProjectBySlug()— シリアライズされたMDXで単一プロジェクトを取得getPageBySlug()— Aboutなどの静的ページを取得
各関数はファイルシステムから読み取り、gray-matterでフロントマターを解析し、next-mdx-remoteでコンテンツをシリアライズ。
フェーズ4: ページ
6つのメインルートを構築:
- Home — 最近の記事リスト付きイントロダクション
- About — MDXから読み込まれる職業経歴
- Blog — 記事一覧と個別記事ページ
- Projects — プロジェクトショーケース(このページ)
- Resume — 職務経歴とスキル
- Contact — SendGrid連携フォーム
フェーズ5: インテグレーション
SendGrid: コンタクトフォームからメールを送信するAPIルートを設定。送信者はSendGridの認証設定で検証が必要。
Google Analytics 4: @next/third-parties経由で追加し、NEXT_PUBLIC_GA_IDに基づく条件付き読み込み。
フェーズ6: デザインシステム
globals.cssにタイポグラフィデザイントークンを実装。ライトモードとダークモード用のOKLCHカラー変数を追加。CSSカスタムプロパティでシマーアニメーションを作成。
フェーズ7: SEO
すべてのページにメタデータを追加。サイトマップとrobots.txtジェネレーターを作成。Person、WebSite、Article、BreadcrumbListスキーマ用のJSON-LD構造化データを実装。
フェーズ8: デプロイ
GitHubにプッシュしてVercelにインポート。環境変数を設定:
NEXT_PUBLIC_SITE_URL— Canonical URLNEXT_PUBLIC_GA_ID— Google AnalyticsSENDGRID_API_KEY— メール配信CONTACT_EMAIL_TO— 受信者アドレスCONTACT_EMAIL_FROM— 検証済み送信者

変更履歴
Gitコミット履歴から抽出した開発の詳細タイムライン。新しい順。
開発統計
- 総コミット数: 51
- 開発期間: 約2日
- 主要機能: 8
- バグ修正: 12
- リファクタリング: 15
- スタイル更新: 16
2025年12月13日 — 2日目: 仕上げと機能追加
夜: 最終調整
| Commit | Description |
|---|---|
20107f4 | Major: 包括的なSEO改善 |
e3ec3c2 | Aboutページのイントロを簡素化 |
c21eb7b | Voice ID設定を一元化 |
9311bc6 | カスタムElevenLabsボイスを設定 |
f8533bf | Major: APIコスト削減のためElevenLabsを最適化 |
f8d31cf | TTSプレーヤーをサマリーの上に配置 |
7da002a | ブログリストのタイポグラフィを更新 |
872a6ba | 明確さとストーリーテリングのためブログ記事を改訂 |
夕方: オーディオ最適化
| Commit | Description |
|---|---|
fc9cea4 | refを使用してHTMLを挿入(ハイドレーション修正) |
fb9303f | Audio Nativeにiframe埋め込みを使用 |
7384478 | MDX互換性のためSuspenseラッパーを復元 |
ff9ca71 | Major: フルコンテンツプロジェクト付きAudio Native API |
76e6e08 | SSR対応Audio NativeのSuspenseを削除 |
36c512c | useEffect経由でAudio Nativeを読み込み |
5073332 | パブリックユーザーIDをハードコード |
ea4e5c7 | Audio Native読み込みにnext/scriptを使用 |
8a5ee20 | Audio Native統合を簡素化 |
32fa6ca | オーディオキャッシュクリアスクリプトを追加 |
61382ca | API呼び出し削減のためオーディオキャッシュを追加 |
0ab2855 | ページ読み込み時にTTS音声を事前生成 |
d59988f | TTSプレーヤーにデバッグログを追加 |
午後遅く: オーディオ統合スプリント
| Commit | Description |
|---|---|
d018e8e | プレーヤーからElevenLabsフッターを削除 |
156eda5 | 速度セレクターをミニマリストに |
b87be4f | プレーヤーからElevenLabsヘッダーを削除 |
1d6af2f | フルオーディオプレーヤーUIを即座に表示 |
1a90ebf | ElevenLabs UIコンポーネントでTTSプレーヤーをアップグレード |
738ebb4 | Audio NativeをTTS APIに置き換え |
fa6fd42 | Cursor IDE設定とルールを追加 |
5152aec | React 19用にMDXRemoteをSuspenseでラップ |
c7b54b2 | Major: ElevenLabs Audio Native TTS統合を追加 |
午後: タイポグラフィ&履歴書
| Commit | Description |
|---|---|
2ff75d4 | ブログ記事の学習記述を更新 |
0c9b17b | 履歴書のタグラインを更新 |
3e6b185 | スキルと学歴項目にダッシュプレフィックスを追加 |
3371a03 | 履歴書から「求めるもの」セクションを削除 |
649010d | 履歴書のタイポグラフィと配置を改善 |
f03743e | リスト項目、リンク、メタテキストのフォントサイズを更新 |
午前遅く: コンテンツ戦略
| Commit | Description |
|---|---|
c5d35e4 | Aboutページにターゲットオーディエンスを追加 |
45e61a2 | 「自分でツールを作る」メッセージをトーンダウン |
b7a8cd8 | 新しいポジショニングのためREADMEを更新 |
ae9f3a3 | 全ページでグロースマーケターポジショニングを洗練 |
701a36f | 「コードが書けるグロースマーケター」としてサイトを再ポジショニング |
db41419 | プロジェクトページを「Coming Soon」プレースホルダーに置き換え |
朝: デザインシステム
| Commit | Description |
|---|---|
4b7f5ff | タイポグラフィトークン、SendGrid、GA4ドキュメントでREADMEを更新 |
d850b7e | ナビメニューを並び替え: About, Blog, Projects, Resume, Contact |
a17536b | Major: タイポグラフィデザイントークンシステムを実装 |
a91398f | ファビコンをターミナルプロンプトアイコン(>_)に更新 |
c85a8c8 | ターミナルロゴのフォーカスリングボーダーを削除 |
af50e11 | ライト/ダークモード対応のためシマーアニメーションをリファクタリング |
2025年12月12日 — 1日目: 基盤構築
| Commit | Description |
|---|---|
0b4e0a2 | Major: 強化UIでSendGridコンタクトフォームを統合 |
5bf8d6a | ターミナルロゴクリック時のテキスト選択を修正 |
2ab20b1 | MDXの静的生成を無効化(React 19互換性修正) |
0d1a813 | ページ、コンポーネント、コア機能を追加 |
891648c | READMEドキュメントをマージ |
08bc5e0 | Major: ブログとプロジェクト構造付き初期Next.jsポートフォリオサイト |
7850991 | GitHubでリポジトリを初期化 |
20aed18 | Create Next Appからの初期コミット |
主要マイルストーン:
- 6ルートの完全なApp Router構造
- gray-matter解析付きMDXコンテンツシステム
- shadcn/uiコンポーネントライブラリ統合
- メール配信付きコンタクトフォーム
- モバイルドロワー付きレスポンシブナビゲーション

スクリーンショット





学んだこと
AI支援開発は本物。 CursorとClaudeがボイラープレートを処理し、エラーをキャッチし、一人では見つけられなかったパターンを提案してくれました。ボトルネックはタイピングから思考へとシフト。
TypeScriptは早期に効果を発揮。 型エラーがブラウザに届く前にバグをキャッチ。初期セットアップ時間が何時間ものデバッグを節約。
デザイントークンはスケールする。 タイポグラフィクラスを一度定義すれば、常に判断せずとも一貫したスタイリングが可能。変更は自動的に伝播。
MDXは柔軟。 フロントマタースキーマがUIを駆動。新しいフィールドの追加はリファクタリングなしで数分で完了。
次のステップ
- ケーススタディとしてプロジェクトを追加
- ブログ記事の検索とフィルタリングを実装
- ブログ購読者向けRSSフィードを追加
- コンテンツ更新のためのISR(Incremental Static Regeneration)を検討
Outcomes
- •AI支援開発(Cursor + Claude)でゼロから構築
- •カスタムタイポグラフィデザイントークンシステムを実装
- •ElevenLabs Audio Nativeによるテキスト読み上げを統合
- •SendGridによるコンタクトフォーム機能を設定
- •包括的なSEO対策でVercelにデプロイ