TL;DR
- GitHub / Google でログイン → プロフィール編集 → プレビュー → 公開リンク発行まで すべて 1 画面で完結する “PortfolioBuilder” を開発した
- React 18 + Vite + Tailwind(+ shadcn/ui)で編集体験を重視した UI、Firebase(Auth / Firestore / Storage / Functions)で安全にデータ管理
- App Check + Cloud Functions v2 callable API で 認証必須のバックエンドを構築しつつ、公開ページは
/p/:slugから誰でも閲覧可能
なぜ作ったのか
私は研究開発系の仕事をしているのですが、転職界隈では
- 「GitHub リンクありますか?」
- 「ポートフォリオ見せてください」
がいまや常識。一方で 自前で静的サイトを作ってメンテするのが面倒という問題があります。
- 更新のたびにコード修正が必要
- 公開 / 非公開切り替えが手間
- 編集と公開の見た目がズレていく
結果、ポートフォリオ準備だけで疲弊するという本末転倒な状態に。そこで
✨「編集 UI」と「公開ページ」をまとめて提供するアプリがあれば、誰でも即座に “伝わるアウトプット” を作れるのでは?
と思い、PortfolioBuilder を作りました。これ自体もポートフォリオとして提示でき、一石二鳥です。
作ったもの(デモ)
PortfolioBuilder
🔗 https://portfoliobuilder-4188f.web.app/

-
左がエディタ / 右がプレビュー の常時同期レイアウト
-
再度押せばワンクリックで 非公開(下書き) に戻せる
画面構成と UX の工夫
ダッシュボード(エディタ × プレビュー)
- 2 カラム構成で「編集したら右側で即反映」
- ヘッダにメールアドレス / 保存状況 / 共有 URL を表示
- 編集中は
hasUnsavedChangesをバッジ表示して 保存し忘れ防止
編集タブ
shadcn/ui の Tabs を使い、以下を横断的に編集可能:
- プロフィール
- 技術スタック
- プロダクト
- 職務経歴
プロダクトや職務経歴は Dialog(モーダル)から CRUD。画像は Storage にライブアップロードし、プログレスも表示する ImageUpload コンポーネントを自作。
公開ページ /p/:slug
- Cloud Functions の REST API からポートフォリオを取得
- エディタと同一コンポーネントで表示するため 編集と公開の見た目が完全一致
- 存在しない slug / 非公開の slug ならエラーページを返す
アーキテクチャと技術スタック
[React (Vite)] --Auth Token--> [Firebase Auth]
│
├─ httpsCallable ───> Cloud Functions v2 (get/update/publish)
│ │
│ └─ Firestore (portfolios, viewCount)
│
├─ Storage SDK ──────> Cloud Storage (avatars / products)
│
└─ HTTP fetch ───────> Cloud Functions v2 (publicPortfolio / incrementView)
│
└─ Firebase Hosting (/p/:slug)
技術スタック
フロントエンド
- React 18 + TypeScript + Vite
- Tailwind CSS + shadcn/ui
- React Query(取得・更新の非同期制御)
Firebase
- Authentication(GitHub / Google)
- Firestore
- Cloud Storage
- Cloud Functions v2
- Hosting
- App Check(reCAPTCHA v3)
開発環境
- Firebase Emulator Suite(Auth / Firestore / Storage / Functions)
実装詳細(ポイント解説)
1. 認証と初期データ生成
-
useAuthフックでonAuthStateChangedを購読し、サインイン後に callable APIgetPortfolioを実行 - Firestore にデータが無ければ空のポートフォリオを自動生成
-
auth/account-exists-with-different-credentialも捕捉し、ユーザーに適切なプロバイダを案内
2. 編集体験の最適化
- Firestore データは一度だけ取得し、以降の編集はローカル state (draft) で完結
- 保存ボタン押下時のみ
updatePortfolioを実行し、無駄な通信を削減 - 「公開する」ボタンに保存処理を内包し、未保存の変更があれば 保存 → 公開 を自動で直列実行
- ImageUpload コンポーネントは 2MB 超を即バリデーションし、
uploadBytesResumableのstate_changedで進捗 UI を更新、完了後にdownloadURLをフォームへ反映
3. プレビュー & 公開ページ
-
PortfolioPreviewをエディタと公開ページで共用し、デザイン差分を物理的に排除 -
/p/:slugでは REST API (publicPortfolio) にX-Firebase-AppCheckヘッダ付きで fetch を実行 - 取得成功後に
incrementPortfolioViewを fire-and-forget で呼び出し、トランザクションで閲覧数を正確に更新
4. Cloud Functions(バックエンド)
- v2 Functions (onCall) で
getPortfolio/updatePortfolio/publishPortfolioを実装し、Auth + App Check を必須化 -
sanitizePortfolioで入力バリデーションを統一し、同じロジックをバックエンドに集約 - 公開 slug は
randomUUID()の 8 文字スライスを採用し、Firestore で重複チェックしてリトライ - 公開 API は
publicPortfolio/incrementPortfolioViewを提供し、後者はトランザクションで view を更新
セキュリティと運用
Firestore Rules
-
portfolios/{id}はrequest.auth.uid == resource.data.uidの場合だけ読み書き可 - 公開状態では
isPublic == trueの場合のみ読み取り許可 -
viewsコレクションは Functions だけが書き込み
Storage Rules
-
/avatars/{uid}/**/products/{uid}/**は本人のみアップロード可 - 表示は公開して問題ない階層に限定
App Check
- フロントで
initializeAppCheckを実行 - Functions 側は
enforceAppCheck: trueまたはヘッダ検証で強制
ログ / 監視
-
functions.loggerを各所で活用し、Cloud Logging / Error Reporting で状況把握
実装しての学び・Tips
- UI と公開ページを 同一コンポーネント で構成すると品質が安定する
- App Check は最初から導入すると後で泣かない
- React Query × ローカル状態は「即時反映 × Firestore 負荷削減」の最適解
- バックエンドにもバリデーションを書くと安心感が段違い
今後追加したい機能
- 公開ページデザインのテンプレート機能
- バージョン履歴とロールバック
- PDF / Markdown エクスポート
- 公開時の Slack / Discord 通知
- カスタムドメイン対応
おわりに
PortfolioBuilder は、「編集と公開で見た目がズレる問題」「公開 / 非公開切り替えが面倒問題」を解消したくて作ったプロダクトです。React × Firebase のシンプル構成でも UI/UX をここまで作り込めるという実感を得られました。
もし「ポートフォリオ作らないとな…」と感じている方がいたら、ぜひ触ってみてください。フィードバックをいただけると嬉しいです!

