FSD × LangChain × Remix × AI でポートフォリオサイトを作ってみた!
みなさん、こんにちは!
「あれ・・・、最近、重力定数変わった?」
…って思うくらい体重増加が進んでいる、フロントエンドエンジニアの @nyaomaru です!
前回の記事では、FSD (Feature-Sliced Design) とは何か?について、概略ではありますが説明させていただきました。
今回は FSD を Remix に適用しつつ、LangChain で AI ターミナル UIを載せたポートフォリオの作り方を、実例ベースでサクッと紹介していきます。
- デモ 👉 https://portfolio-nyaomaru.vercel.app/
- リポジトリ 👉 https://github.com/nyaomaru/nyaomaru-portfolio-sample
ほな、一緒に見ていこな!
🎬 要件定義
まずは
「どんなポートフォリオサイト作ろか~?」
ってなるわけやけども、そこは各々の感性で好きなもの作っていけばええ。
ワイは今回、ターミナル形式でやり取りできる自己紹介サイトを作ってみたいと思った。
理由としては、最近 Claude を使ってるときに、
ターミナルっぽい UI で返事をもらうと、なんか「特別感」あらへん?
「コマンド打ってる感 × AI からの返答」って、かっこええやん?
ええやんええやん、すてきやん!?ってなったからやな。
でも、シンプルなターミナルやとおもんないから、AI を用いて柔軟に返答できるようにしたいなぁ~
ほんで、それだけやったらさみしいから、ワイの作ったもんやスキルとかを紹介するページと、ワイの Zenn の記事へのリンクを紹介するページも作ろうと思っとる。
つまり、以下のような構成やな
pages/
top/ # ターミナル形式で自己紹介できるもの。AIが質問を理解し、回答する
profile/ # スキルやOSSなどの紹介を行う画面
articles/ # 記事へのリンクが一覧になっている画面
🖋️ 技術選定
「どんな技術で実現しよっかな~?」って考えるの楽しない?ワイは結構好きや!
今回、FSD の公式が推しているのと単純に触ってみたいという理由でRemix
を採用したで~!
ほんで UI library はスピード重視で tailwind base の shadcn
を採用や!この子は CLI でバシーンとコンポーネント作れるし、AI とも相性ええし、ええなぁ~やっぱすっきゃねん。
API は OpenAI に POST 叩くだけのシンプル構成やから、fetch
で表現しよか。
あと、AI にただリクエストするだけやったらおもんないから、LangChain
を用いて、簡単チェーン(前処理->LLM->後処理)させて送ってみよ。
ほんで以下のよう構成になったで!やっぱ。技術構成考えるの、楽しいよなぁ~!あと、zod
とか type-fest
はお好みで!
Remix + vite + shadcn + fetch + LangChain
LangChain
LangChain の実装イメージは以下のような感じやな。データを前処理してコンテキストを生成することで、適切な回答を得られる確率があがる、っちゅう戦法や。
処理の流れはこんな感じやで
question
↓
getProfileDocs
↓
keyword match + embedding match
↓
context
↓
RunnableSequence(prompt + model)
↓
answer
実装はこんな感じにしてみるで~
export async function makeProfileQAChain(
apiKey: string,
question: string
): Promise<AIMessageChunk> {
// 🔍 前処理:public/profile.json を読み込む
const docs = await getProfileDocs();
// 🧠 前処理:キーワード or 埋め込みで文脈抽出
const vectorStore = await MemoryVectorStore.fromDocuments(
docs,
getEmbeddings(apiKey)
);
const retriever = vectorStore.asRetriever();
const embeddingMatches = await retriever.invoke(question);
// 質問に含まれるキーワードを見て、プロフィールから関連しそうな部分を抽出する
const keywordMatches = getRelatedProfileChunks(question, docs);
const combinedContext = [
...embeddingMatches.map((document) => document.pageContent),
...keywordMatches,
];
// 📝 コンテキスト生成
const context = Array.from(new Set(combinedContext)).join('\n');
// 💬 プロンプトを組み立て、モデルに質問
const prompt = profileQAPrompt;
const model = getChatModel(apiKey);
const chain = RunnableSequence.from([prompt, model]);
return chain.invoke({ question, context });
}
詳しくは公式ドキュメント読んでみてな~!
📃 設計
ほな設計していこか~。
今回、お勉強の意味も込めて FSD で構築しよや!まずは、Layer 構成を決めよか~
app/
Remix の router を宣言する
pages/
top / profile / articles の画面を表現する
widgets/
top 画面で用いる terminal や 全画面で利用する header コンポーネントを宣言する
features/
LangChain を用いた OpenAI へのAPI処理を記述する
entities/
この規模では導入しないと判断
shared/
shadcn を用いた共通コンポーネントや共通処理を配置する
entities
は今回の規模で無理やり使うと、余計にややこしくなりそうやったからお見送りや!
さて、ここまでの要件・技術選定・設計は考えておわり~やなくて、Markdown で書いとこか。これが AI との開発時に肝になってくるんやで~!
🔨 開発準備
さあ、お楽しみの開発や!
ほんなら今回は Cursor くんに頑張ってもらおかな。
まずは、ここまで記載した内容を、.cursor/rules
に prompt として落とし込んでいくで。
なんでこれ作るかというと、前振りがあるのとないので、成果物の質が全然変わってまうからやねん。
以下のようなフロー図をイメージしとる。
Developer
│
▼
.cursor/rules/*.md
│ (設計方針 / UI指針 / 命名ルール)
▼
Cursor へ作業指示
│
▼
AI が指針を読み込み、コード生成
AI にとっても 設計・命名ルール・コンポーネント構成は「前提知識」 やから、ここで伝えておかへんと、毎回ズレた実装になるんや。
せやから、こうやって設計や指針を先に作ってあげることで、AI がより開発しやすくなるで。
試しに、以下のようなファイル構成で prompt を作ってみたで!
.cursor/rules/
architecture.mdc cursor の知っているFSDと認識齟齬がないように、再定義する。特に今回作成する成果物の方針と合わせる
components_guidelines.mdc 勝手に新しい component を作られないように shadcn を用いることを明記する
cording_standards.mdc 基本的な coding rule を記載しておく。
portfolio_plan.mdc 今回の設計した内容をここに記載する。
test.mdc テストに関する指示を記載する。個人開発のポートフォリオなのでロジック周りだけね。
細かい所は各自で調整してもろたらええとして、FSD の汎用的なアーキの説明をここに貼っておくから、これ使ってもろたら FSD での開発を再現できると思う。
概要だけ乗せてるけど、詳細は実装を見てな~(英語のほうが精度高いから、実際に使うときは英語にしてるで) 👉
---
alwaysApply: true
---
# architecture.mdc
## アーキテクチャ・設計
本プロジェクトは Feature-Sliced Design (FSD) を厳格に適用し、各レイヤーの責務・依存ルールを明確に定義することで、拡張性・保守性・再現性を最大化します。
## レイヤー構成と責務
### 1. Shared Layer (`shared/`)
- 全レイヤーで再利用可能なUIコンポーネント・ユーティリティ・定数・型・設定
- 例: Button, Card, Typewriter, 共通hooks, APIクライアント
- **依存可能:なし(最内層)**
### 2. Entities Layer (`entities/`)
- ビジネスエンティティ(例:Project, Experience, Skill)
- エンティティ固有の型・UI・ローダ・アクション
- **依存可能:shared**
### 3. Features Layer (`features/`)
- ユーザー操作やビジネスロジック(例:ターミナルのコマンド処理)
- feature固有のUI・hooks・ローダ・アクション
- **依存可能:entities, shared**
### 4. Widgets Layer (`widgets/`)
- 複数featureやentity, sharedを組み合わせた複合UI(例:Header, Terminal)
- レイアウトや大きなUIブロック
- **依存可能:features, entities, shared**
### 5. Pages Layer (`pages/`)
- 各画面(ページ)の基本となる実装を担当
- 画面ごとに`ui/`(画面を構成するUIコンポーネント)、`model/`(画面固有の状態・ロジック・データ取得)などをディレクトリ内に定義
- 例: `pages/top/`, `pages/profile/`, `pages/articles/` など
- **依存可能:widgets, features, entities, shared**
- **依存禁止:他のpages**
- **Remixのroutesとは分離し、画面単位の責務を明確化**
### 6. App Layer (`app/routes/`)
- Remixのfile-based routingに従い、各ページ(ルート)を定義
- 各ルートで`pages/`配下の画面実装を呼び出し、ルート固有のローダ・アクション・エラーバウンダリ・meta関数を実装
- **依存可能:pages, widgets, features, entities, shared**
- **依存禁止:他のroutes**
- 例:`app/routes/_index.tsx`, `app/routes/profile.tsx`, `app/routes/articles.tsx`
## 依存ルール(Dependency Rule)
- 依存は必ず「外側→内側」のみ
- 逆方向の依存は禁止(例:sharedからfeaturesへの依存はNG)
- 依存関係の図:
[app/routes] → [pages] → [widgets] → [features] → [entities] → [shared]
- 具体例:
- pagesはwidgets, features, entities, sharedに依存可
- widgetsはfeatures, entities, sharedに依存可
- featuresはentities, sharedに依存可
- entitiesはsharedのみ依存可
- sharedは他レイヤーに依存不可
- app/routesはpages, widgets, features, entities, sharedに依存可
- app/routes同士、pages同士の依存は禁止
また、shadcn
で開発するにおいて、FSD と適合できるように components.json
を設定しておくと、AI が開発しやすくなると思うわ。
aliases
の設定で、shared
の階層を指定しておくことで、シュッとコンポーネント追加できるで~!
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/tailwind.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "shared/ui",
"utils": "shared/lib/css",
"ui": "shared/ui",
"lib": "shared/lib",
"hooks": "shared/hooks"
},
"iconLibrary": "lucide"
}
🧑💻 開発
ここまで前振りした後で、実装を Cursor にお願いしていくと、たぶんうまくいくで! (LLM の model は gpt-4.1
を使ってみてたで)
頼むときは、画面単位でお願いしちゃうと、実装がブレちゃうから、
pages/articles
/ widgets/header
/ features/terminal
のように機能単位で作業をお願いして進めていくのが、個人的には良かった!
でも AI との開発、いろんな事件が起きる。
例えば:
- 指示してないのに謎のボタンを勝手に追加される
- LangChain の関数名が突然ポエムみたいになる(
meltMemoriesIntoOneTruth
とか) - shadcn コンポーネント作成のコマンドが古い&失敗して公式ドキュメントからコピペしてくる
LLM で一発 OK は稀や。指摘して育てるつもりで付き合うスタンスがええと思う。
自然言語だけでもある程度の成果物は作ってくれるから、そこからの調整とかエラーの修正は AI に頼んでもええし、頼むほうがコストになることもあるから、シュッと直せるとこは自分で対応したほうがええなぁ~
✨ 仕上げ
作成したコードをリファクタしていこか。
このフェーズでワイが結構やるのは、VSCode
使って、自分で修正しながら、平行してCopilot
にシュッと直させるな。
理由としては、軽微な修正を依頼するコストよりも、修正箇所指定してCopilot
のカスタムコマンドで頼む or 自分で直すほうが、現状では早いからやな。
例えば、/jsdoc
って打ったときに、該当箇所にコメントをつけてくれるようにするには、
---
mode: edit
---
- JSDoc を日本語で書いてください
- Example は不要です
- description ではなく、本文に説明を記載してください
てな感じで定義したファイルを、.github/prompts/
に配置しておく。
ほんで、修正範囲を選択して Command + i
/ Ctrl + i
を実行し、/xxx
って打つと実行してくれるで~。
作ったカスタムコマンドは、チームで共有したら再利用できるからええ感じやで~!
仕上がったら Vercel 使って deploy しよか。(Settings -> Environment Variables で利用する API key を忘れんようにな!)
よっしゃ、完成や!!!
まとめ
FSD は AI にも理解できるアーキテクチャで、これからの時代の可能性を感じた。
特に、AI に tactical な実装をお願いし続けると、構成がぐちゃぐちゃなるし、気づいたら読みにくいコードが量産されてまう。AI による技術的負債が加速度的に増えてまう。
AI による技術的負債っちゅうのは以下のようなものをイメージしとる
- 既存コードの設計に引きずられてしまい、規模に耐えられない構造のままのコードが量産される
- 人間にとって読みやすいコードではなく、短縮記法連発してたり、パフォーマンス重視のコードが量産される
せやから、事前に設計して正しい指示を prompt で書き起こしておくと、技術的負債の懸念もある程度解消できるし、開発スピード爆速なるし、なにより一緒に開発してて楽しいで~!!!
おまけ
(再掲)この記事で紹介した構成のソースコードは、サンプルとして公開しております~ 👇
あと、「設計してお願いして作って終わり~」じゃなくて、作る過程で再設計したり、prompt を見直したり、リファクタリングするのはすごく大切だと思います。
絶対的な正解はないので、常に修正しつづけて incremental な開発を持続可能にすることが、今後も大切なんじゃないかな?知らんけど。
次回予告
最近、Unity でゲーム開発やってるときに、「typescript でもゲーム作ってみよかなぁ~」って思ったから、次は Typescript で簡単なゲームを作ってみた って記事を書く予定やで。期待せんとまっててな~!
他の記事の紹介
他の記事もあるから、見てってな~
- 📘 実務コードベースのサンプル付き!
- 📘 フォルダ構成・責務設計に効く話!
- 📘 根本からわかる“負債とは何か”
またな!
Discussion