「エンジニアのポートフォリオって、結局何を作れば評価されるの?」「Next.jsで作るのが今は正解?」「採用担当者って、実際にコードのどこを見てるの?」——本記事は、未経験〜中堅エンジニアが転職・案件獲得で武器になるポートフォリオサイトの設計から実装、デプロイ、評価される書き方までを、現役エンジニア(週5フルリモート/年収900万超/エンジニア採用面接100名以上経験)の視点で完全網羅したものです。コード例25個・チェックリスト・採用担当者の評価軸・やってはいけない事まで、6,000字超で徹底解説します。
本記事は、ポートフォリオを「とりあえず作って公開する」レベルから「Web系自社開発企業の選考通過率を3倍にする」レベルに引き上げるための実践ガイドです。Next.js 15 + Tailwind CSS 4を使った具体的な実装コードを15本以上掲載し、README・GitHub整備・SEO・Vercelデプロイまで一気通貫で解説します。なお、転職活動の流れ自体はフロントエンドエンジニア学習ロードマップ2026とプログラミングスクール完全比較2026でカバーしていますので、合わせて読むと体系が完成します。
・必須3要素:技術スタック明示・コード品質(GitHub公開)・READMEで「なぜそれを作ったか」を語る
・採用担当者が見るのは:見た目より「コミット履歴」「Issue/PR運用」「README」「型安全性」「テスト」
・技術スタック推奨:Next.js 15 (App Router) + TypeScript + Tailwind CSS 4 + Vercel
・NG行動:ChatGPTで生成しただけのコード・コメントゼロ・READMEが「Getting Started」だけ
・最短工数:設計2日 + 実装5日 + デプロイ1日 = 1週間で公開可能(土日 + 平日夜のペース想定)
- 1. ポートフォリオの重要性〜採用担当者の視点で全部書く〜
- 2. 何を作るか〜レベル別の題材選定と差別化戦略〜
- 3. 必要な要素〜技術スタック・コード品質・README〜
- 4. Next.js 15 + Tailwind でポートフォリオサイト作成
- 5. 実装〜Header/Hero/Skills/Works/Contact〜
- 6. SEO対策〜ポートフォリオでも検索流入を意識する〜
- 7. デプロイ〜Vercelに上げて初公開〜
- 8. GitHub整備〜採用担当者が見るのは結局ここ〜
- 9. READMEの書き方〜採用担当者の30秒で全部伝える〜
- 10. READMEテンプレート(コピペで使える)
- 11. アピールポイントの言語化〜面接で勝つための準備〜
- 12. やってはいけない事〜採用担当者が即落とすNG集〜
- 13. 各社別ポートフォリオ評価軸〜企業タイプで見られる場所が違う〜
- 14. ChatGPT/Copilot活用しすぎNG〜「AIに作らせた感」は秒でバレる〜
- 15. FAQ〜よく聞かれる質問に全部答える〜
- 16. まとめ〜ポートフォリオは「完成」より「公開して更新し続ける」〜
1. ポートフォリオの重要性〜採用担当者の視点で全部書く〜
1.1 なぜ職務経歴書だけでは不十分なのか
多くの応募者が誤解しているのは、「職務経歴書に技術スタックを書けばエンジニアとして評価される」という前提です。採用担当者の本音は「書いてある技術を本当に使いこなせるかは、コードを見ないと判断できない」です。特にWeb系自社開発企業では書類選考で6〜7割が落ちますが、その理由の半分は「コードを見せる手段がないため判断できなかった」という消極的不合格です。
1.2 ポートフォリオが転職活動で果たす役割
| フェーズ | ポートフォリオの役割 | 書類のみとの差 |
|---|---|---|
| 書類選考 | 「コードが書ける証拠」として通過率を底上げ | 通過率+25〜40% |
| カジュアル面談 | 会話の話題提供・質問の解像度向上 | 面談時間の質が2倍 |
| 技術面接 | コードレビュー形式で進行・コーディング試験を免除されることも | 面接通過率+30〜50% |
| 最終面接 | 「この人ならプロダクト作れる」確信材料 | 内定確率+20% |
| 年収交渉 | スキルの可視化により提示額の妥当性を主張可能 | 年収+50〜100万 |
1.3 採用担当者が30秒で見る場所
採用担当者(特に現役エンジニア面接官)は、応募者のポートフォリオURLを開いてから30秒で「読む価値があるか」を判断します。私が100名以上面接した経験から、その30秒で見られる順番は次の通りです。
| 順番 | 見る場所 | 判断ポイント |
|---|---|---|
| 1秒目 | サイトのファーストビュー | レイアウト崩れ・モバイル対応・ロード速度 |
| 5秒目 | 使用技術スタックの一覧 | 応募職種と合致しているか・モダンか |
| 10秒目 | GitHubリポジトリへのリンク | そもそも公開されているか・草の量 |
| 15秒目 | READMEの最初の3行 | 「何を作ったか」「なぜ作ったか」が書いてあるか |
| 25秒目 | コミット履歴(最近10件) | 「Initial commit」だけで終わってないか |
| 30秒目 | ディレクトリ構成 | 意図のある設計か・コピペ感が強くないか |
1.4 評価される人と落ちる人の決定的な違い
評価される応募者は「ポートフォリオを作る過程そのものを言語化できる」人です。「なぜこの技術を選んだか」「最初は何を作ろうとして、どこで詰まって、どう解決したか」を語れる人は、面接でほぼ落ちません。逆に「とりあえずチュートリアル通りに作りました」型は、技術スタックが派手でも見抜かれます。採用担当者が知りたいのは技術ではなく、技術選定の思考プロセスです。
2. 何を作るか〜レベル別の題材選定と差別化戦略〜
2.1 レベル別の推奨題材
| 経験レベル | 推奨題材 | 差別化ポイント |
|---|---|---|
| 未経験 | 自分用ポートフォリオサイト + 小規模CRUDアプリ1本 | 「自分の困りごと」をテーマに選ぶ |
| 初学者(3〜6ヶ月) | SaaS型ミニサービス(タスク管理・読書記録・家計簿) | 認証・DB・APIを全て触る |
| 初級エンジニア(1〜2年) | 外部APIを活用した実用アプリ(天気/書籍/地図/AI) | 非同期処理・キャッシュ・エラー処理を作り込む |
| 中堅(2〜5年) | マルチテナントSaaS or リアルタイムアプリ | テスト・CI/CD・監視・パフォーマンス計測 |
| シニア候補(5年+) | 業務効率化ツール+OSS化 or 技術ブログ自前実装 | 設計判断の言語化・スケーラビリティ考慮 |
2.2 「自分の困りごと」起点で選ぶべき理由
未経験者ほどTodoアプリやチャットアプリに走りがちですが、これらは採用担当者が見飽きている代表格です。差別化するには「自分が日常で困っていること」を起点に選びましょう。例えば「読書記録を本棚に置けるサービス」「家族の予定を1画面に集約するカレンダー」「副業の請求書を自動生成するツール」など、ニッチでも自分が使うものは熱量が違います。面接で「なぜこれを作ったか」を聞かれたとき、自然に語れる題材であることが最重要です。
2.3 ポートフォリオサイト本体と「制作物」は別物
混同されがちですが、「ポートフォリオサイト」と「制作物」は別物です。ポートフォリオサイトはあなた自身の名刺・カタログで、制作物はそこに掲載される作品です。本記事の3〜5章はこの「カタログ」部分の実装、6〜10章はその両方の品質を上げる話、11〜16章はキャリア戦略になります。
3. 必要な要素〜技術スタック・コード品質・README〜
3.1 2026年時点の標準技術スタック
| レイヤー | 第一候補 | 第二候補 | 理由 |
|---|---|---|---|
| フレームワーク | Next.js 15 | Astro / SvelteKit | 採用企業の利用率が圧倒的 |
| 言語 | TypeScript | JavaScript | 2026年は型なしは減点対象 |
| スタイリング | Tailwind CSS 4 | CSS Modules / Panda CSS | shadcn/uiとの相性が良い |
| UIコンポーネント | shadcn/ui | Radix UI / Mantine | カスタマイズしやすい |
| 状態管理 | Zustand / TanStack Query | Jotai / Redux Toolkit | 軽量・モダン |
| フォーム | React Hook Form + Zod | Conform | 型安全と相性抜群 |
| DB(必要時) | PostgreSQL + Prisma | SQLite + Drizzle | 本番運用に近い |
| 認証(必要時) | Auth.js v5 (NextAuth) | Clerk / Lucia | 無料&拡張性高い |
| デプロイ | Vercel | Cloudflare Pages | Next.jsとの相性が最強 |
| CI/CD | GitHub Actions | CircleCI | GitHub完結で楽 |
| テスト | Vitest + React Testing Library | Jest + Playwright | ESM時代の標準 |
関連解説はNext.js 15完全実践ガイド、TypeScript型推論完全ガイド、React Testing Library完全実践ガイドを参照してください。
3.2 コード品質を担保する設定一覧
| ツール | 役割 | 採用面接での評価 |
|---|---|---|
| ESLint 9 (Flat Config) | コード規約の自動チェック | あれば「設定できる人」と判定 |
| Prettier 3 | フォーマット統一 | 無いと「読みづらい」と感じられる |
| Husky + lint-staged | コミット前の自動チェック | 「チーム開発意識あり」と高評価 |
| tsconfig strict: true | 型の厳格化 | 無いと「TypeScript分かってない」 |
| Vitest | ユニットテスト | あると即「中堅以上」判定 |
| Storybook 8 | UIカタログ | あればフロント職で大幅加点 |
これらの設定方法はESLint 9 + Prettier 3完全設定ガイド、Husky + lint-staged完全設定ガイド、tsconfig.json完全ガイドに詳細を載せています。
3.3 READMEに最低限書くべき7項目
| 項目 | 内容 | 採用評価 |
|---|---|---|
| 1. プロジェクト概要 | 1段落で「何を解決するか」 | 無いと即離脱 |
| 2. スクリーンショット | 3〜5枚 or GIF | 視覚情報で差別化 |
| 3. 技術スタック | バッジ表示 or 表 | 応募職種との合致確認 |
| 4. アーキテクチャ | 図1枚 or 構成説明 | 設計力の証明 |
| 5. ローカル起動手順 | 3行で動かせるレベル | 面接官が手元で動かす |
| 6. 工夫した点・苦労した点 | 2〜3項目を100字ずつ | 面接の話題提供 |
| 7. 今後の改善予定 | 未完成項目を正直に書く | 誠実さで好印象 |
4. Next.js 15 + Tailwind でポートフォリオサイト作成
4.1 プロジェクト初期化
2026年時点での標準的なプロジェクト初期化手順です。Next.js 15はApp Router・RSC・Server Actionsが標準なので、create-next-app一発で全部入りの環境ができます。
# Next.js 15 + TypeScript + Tailwind + App Router で初期化
pnpm create next-app@latest portfolio
--typescript --tailwind --app --src-dir --import-alias "@/*"
--eslint --use-pnpm
cd portfolio
# shadcn/ui を追加(2026年時点はshadcn CLIが標準)
pnpm dlx shadcn@latest init
# 必須コンポーネントをまとめてインストール
pnpm dlx shadcn@latest add button card badge separator sheet
# 開発サーバー起動
pnpm dev
4.2 ディレクトリ構成
portfolio/
├── src/
│ ├── app/
│ │ ├── layout.tsx # ルートレイアウト
│ │ ├── page.tsx # トップページ
│ │ ├── works/
│ │ │ ├── page.tsx # 制作物一覧
│ │ │ └── [slug]/page.tsx # 制作物詳細
│ │ ├── about/page.tsx # プロフィール詳細
│ │ └── contact/page.tsx # お問い合わせ
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── Hero.tsx
│ │ ├── Skills.tsx
│ │ ├── Works.tsx
│ │ ├── Contact.tsx
│ │ └── ui/ # shadcn/uiの自動生成
│ ├── lib/
│ │ ├── works.ts # 制作物データ
│ │ └── skills.ts # スキルデータ
│ └── types/
│ └── work.ts
├── public/
│ ├── og-image.png
│ └── works/ # 制作物のスクショ
├── tailwind.config.ts
├── tsconfig.json
└── package.json
4.3 ルートレイアウト(layout.tsx)
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter, Noto_Sans_JP } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/Header";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const noto = Noto_Sans_JP({ subsets: ["latin"], variable: "--font-noto" });
export const metadata: Metadata = {
title: {
default: "Yamada Taro | Frontend Engineer Portfolio",
template: "%s | Yamada Taro Portfolio",
},
description:
"現役フロントエンドエンジニア山田太郎のポートフォリオサイト。Next.js / TypeScript / React を中心に、Web系自社開発SaaSを開発しています。",
openGraph: {
title: "Yamada Taro | Frontend Engineer Portfolio",
images: ["/og-image.png"],
type: "website",
},
twitter: { card: "summary_large_image" },
};
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="ja" className={`${inter.variable} ${noto.variable}`}>
<body className="bg-white text-zinc-900 antialiased">
<Header />
<main className="mx-auto max-w-5xl px-4 py-12">{children}</main>
</body>
</html>
);
}
5. 実装〜Header/Hero/Skills/Works/Contact〜
5.1 Headerコンポーネント
// src/components/Header.tsx
import Link from "next/link";
const NAV = [
{ href: "/", label: "Home" },
{ href: "/works", label: "Works" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
];
export function Header() {
return (
<header className="sticky top-0 z-50 border-b border-zinc-200 bg-white/80 backdrop-blur">
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-4">
<Link href="/" className="text-lg font-bold tracking-tight">
Yamada Taro
</Link>
<nav className="flex gap-6 text-sm">
{NAV.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-zinc-600 hover:text-zinc-900">
{item.label}
</Link>
))}
</nav>
</div>
</header>
);
}
5.2 Heroセクション
// src/components/Hero.tsx
import Link from "next/link";
import { Button } from "@/components/ui/button";
export function Hero() {
return (
<section className="py-16 sm:py-24">
<p className="text-sm font-medium text-blue-600">Frontend Engineer</p>
<h1 className="mt-3 text-4xl font-bold tracking-tight sm:text-5xl">
Webで「使ってよかった」と
<br className="hidden sm:block" />
言われる体験を作る。
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-zinc-600">
Next.js / TypeScript / React を中心に、
BtoB SaaS と toC サービスのフロントエンド開発に従事しています。
パフォーマンス・アクセシビリティ・型安全性を3点セットで重視します。
</p>
<div className="mt-8 flex gap-3">
<Button asChild>
<Link href="/works">制作物を見る</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/contact">お問い合わせ</Link>
</Button>
</div>
</section>
);
}
5.3 Skillsセクション
// src/lib/skills.ts
export type SkillLevel = 1 | 2 | 3 | 4 | 5;
export type Skill = {
name: string;
level: SkillLevel;
category: "language" | "frontend" | "backend" | "infra" | "tool";
};
export const SKILLS: Skill[] = [
{ name: "TypeScript", level: 5, category: "language" },
{ name: "JavaScript", level: 5, category: "language" },
{ name: "React", level: 5, category: "frontend" },
{ name: "Next.js", level: 4, category: "frontend" },
{ name: "Tailwind CSS", level: 4, category: "frontend" },
{ name: "Node.js", level: 4, category: "backend" },
{ name: "Prisma", level: 3, category: "backend" },
{ name: "PostgreSQL", level: 3, category: "backend" },
{ name: "Vercel", level: 4, category: "infra" },
{ name: "GitHub Actions", level: 3, category: "infra" },
{ name: "Figma", level: 3, category: "tool" },
];
// src/components/Skills.tsx
import { SKILLS } from "@/lib/skills";
const CATEGORY_LABEL = {
language: "言語",
frontend: "フロントエンド",
backend: "バックエンド",
infra: "インフラ",
tool: "ツール",
} as const;
export function Skills() {
const grouped = SKILLS.reduce<Record<string, typeof SKILLS>>(
(acc, skill) => {
acc[skill.category] ??= [];
acc[skill.category].push(skill);
return acc;
},
{},
);
return (
<section className="py-12">
<h2 className="text-2xl font-bold">Skills</h2>
<div className="mt-6 grid gap-6 sm:grid-cols-2">
{Object.entries(grouped).map(([category, skills]) => (
<div key={category}>
<h3 className="text-sm font-semibold text-zinc-500">
{CATEGORY_LABEL[category as keyof typeof CATEGORY_LABEL]}
</h3>
<ul className="mt-3 space-y-2">
{skills.map((s) => (
<li key={s.name} className="flex items-center justify-between">
<span>{s.name}</span>
<span className="text-xs text-zinc-500">{"★".repeat(s.level)}</span>
</li>
))}
</ul>
</div>
))}
</div>
</section>
);
}
5.4 Worksセクション(制作物カード一覧)
// src/lib/works.ts
export type Work = {
slug: string;
title: string;
description: string;
thumbnail: string;
url?: string;
repo?: string;
stack: string[];
highlights: string[];
};
export const WORKS: Work[] = [
{
slug: "reading-shelf",
title: "Reading Shelf",
description: "読書記録を本棚UIで管理できるSaaSアプリ。",
thumbnail: "/works/reading-shelf.png",
url: "https://example.com",
repo: "https://github.com/yamada/reading-shelf",
stack: ["Next.js 15", "TypeScript", "Prisma", "PostgreSQL", "Auth.js"],
highlights: [
"Server Actionsでフォーム送信を実装(JS無効でも動作)",
"PrismaのトランザクションでN+1を回避",
"Vitest + Playwrightで主要導線を全てカバー",
],
},
{
slug: "expense-bot",
title: "Expense Bot",
description: "LINE経由で家計簿が付けられるBotサービス。",
thumbnail: "/works/expense-bot.png",
repo: "https://github.com/yamada/expense-bot",
stack: ["Hono", "Cloudflare Workers", "D1", "LINE Messaging API"],
highlights: [
"Cloudflare Workers + Hono で低レイテンシ(p95: 80ms)",
"Zodで入力バリデーション・型推論を両立",
"GitHub Actionsで自動デプロイ+Slack通知",
],
},
];
// src/components/Works.tsx
import Link from "next/link";
import Image from "next/image";
import { WORKS } from "@/lib/works";
export function Works() {
return (
<section className="py-12">
<h2 className="text-2xl font-bold">Works</h2>
<div className="mt-6 grid gap-6 sm:grid-cols-2">
{WORKS.map((work) => (
<Link
key={work.slug}
href={`/works/${work.slug}`}
className="group rounded-lg border border-zinc-200 p-4 hover:border-blue-500">
<div className="relative aspect-video overflow-hidden rounded">
<Image
src={work.thumbnail}
alt={work.title}
fill
className="object-cover transition group-hover:scale-105"
/>
</div>
<h3 className="mt-4 font-semibold">{work.title}</h3>
<p className="mt-1 text-sm text-zinc-600">{work.description}</p>
<ul className="mt-3 flex flex-wrap gap-1.5">
{work.stack.map((tech) => (
<li key={tech} className="rounded bg-zinc-100 px-2 py-0.5 text-xs">
{tech}
</li>
))}
</ul>
</Link>
))}
</div>
</section>
);
}
5.5 制作物詳細ページ
// src/app/works/[slug]/page.tsx
import { notFound } from "next/navigation";
import Image from "next/image";
import { WORKS } from "@/lib/works";
export function generateStaticParams() {
return WORKS.map((w) => ({ slug: w.slug }));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const work = WORKS.find((w) => w.slug === slug);
return work ? { title: work.title, description: work.description } : {};
}
export default async function WorkDetail({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const work = WORKS.find((w) => w.slug === slug);
if (!work) notFound();
return (
<article className="py-12">
<h1 className="text-3xl font-bold">{work.title}</h1>
<p className="mt-3 text-zinc-600">{work.description}</p>
<div className="relative mt-8 aspect-video overflow-hidden rounded-lg">
<Image src={work.thumbnail} alt={work.title} fill className="object-cover" />
</div>
<h2 className="mt-10 text-xl font-bold">工夫したポイント</h2>
<ul className="mt-4 list-disc space-y-2 pl-6">
{work.highlights.map((h) => <li key={h}>{h}</li>)}
</ul>
<div className="mt-10 flex gap-3">
{work.url && <a className="underline" href={work.url} target="_blank" rel="noopener">Demo</a>}
{work.repo && <a className="underline" href={work.repo} target="_blank" rel="noopener">GitHub</a>}
</div>
</article>
);
}
5.6 Contact(Server Actions)
// src/app/contact/actions.ts
"use server";
import { z } from "zod";
const ContactSchema = z.object({
name: z.string().min(1, "名前は必須です"),
email: z.string().email("メールアドレスが不正です"),
message: z.string().min(10, "10文字以上で入力してください"),
});
export type ContactState = { ok: boolean; errors?: Record<string, string> };
export async function submitContact(
_prev: ContactState,
formData: FormData,
): Promise<ContactState> {
const parsed = ContactSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
const errors: Record<string, string> = {};
for (const issue of parsed.error.issues) {
errors[String(issue.path[0])] = issue.message;
}
return { ok: false, errors };
}
// 実運用ではここでメール送信 or Slack通知
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
body: JSON.stringify({ text: `New contact: ${parsed.data.email}` }),
});
return { ok: true };
}
// src/app/contact/page.tsx
"use client";
import { useActionState } from "react";
import { submitContact, type ContactState } from "./actions";
const initial: ContactState = { ok: false };
export default function ContactPage() {
const [state, action, pending] = useActionState(submitContact, initial);
if (state.ok) {
return <p className="py-12">送信が完了しました。ありがとうございました。</p>;
}
return (
<form action={action} className="space-y-4 py-12">
<input name="name" placeholder="お名前"
className="w-full rounded border px-3 py-2" />
{state.errors?.name && <p className="text-sm text-red-600">{state.errors.name}</p>}
<input name="email" placeholder="メール"
className="w-full rounded border px-3 py-2" />
{state.errors?.email && <p className="text-sm text-red-600">{state.errors.email}</p>}
<textarea name="message" rows={5} placeholder="ご用件"
className="w-full rounded border px-3 py-2" />
{state.errors?.message && <p className="text-sm text-red-600">{state.errors.message}</p>}
<button disabled={pending}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50">
{pending ? "送信中..." : "送信"}
</button>
</form>
);
}
Server Actions・useActionStateの基礎はReact Server Components完全ガイド、フォームのバリデーション設計はZod完全実践ガイド、React Hook Form完全実践ガイドを参照してください。
6. SEO対策〜ポートフォリオでも検索流入を意識する〜
6.1 動的メタデータ生成
// src/app/works/[slug]/page.tsx (再掲・要点)
export async function generateMetadata({ params }) {
const { slug } = await params;
const work = WORKS.find((w) => w.slug === slug);
if (!work) return {};
return {
title: work.title,
description: work.description,
openGraph: {
title: work.title,
description: work.description,
images: [work.thumbnail],
},
alternates: { canonical: `/works/${work.slug}` },
};
}
6.2 sitemap.xmlとrobots.txt
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
import { WORKS } from "@/lib/works";
const BASE = "https://example.com";
export default function sitemap(): MetadataRoute.Sitemap {
const works = WORKS.map((w) => ({
url: `${BASE}/works/${w.slug}`,
lastModified: new Date(),
}));
return [
{ url: BASE, lastModified: new Date() },
{ url: `${BASE}/works`, lastModified: new Date() },
{ url: `${BASE}/about`, lastModified: new Date() },
{ url: `${BASE}/contact`, lastModified: new Date() },
...works,
];
}
// src/app/robots.ts
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: `${BASE}/sitemap.xml`,
};
}
6.3 OGP画像の動的生成
// src/app/works/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
import { WORKS } from "@/lib/works";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({ params }: { params: { slug: string } }) {
const work = WORKS.find((w) => w.slug === params.slug);
return new ImageResponse(
(
<div style={{
display: "flex", flexDirection: "column",
width: "100%", height: "100%",
background: "#0f172a", color: "white",
padding: 80, justifyContent: "center",
}}>
<div style={{ fontSize: 32, opacity: 0.7 }}>Yamada Taro / Works</div>
<div style={{ fontSize: 72, fontWeight: "bold", marginTop: 24 }}>
{work?.title ?? "Untitled"}
</div>
</div>
),
size,
);
}
7. デプロイ〜Vercelに上げて初公開〜
7.1 Vercelへのデプロイ手順
# GitHubへ初期Push
git init
git add .
git commit -m "feat: initial portfolio site"
git branch -M main
git remote add origin git@github.com:yamada/portfolio.git
git push -u origin main
# Vercel CLIで連携(対話形式で進む)
pnpm dlx vercel
# > Set up and deploy "./portfolio"? [Y/n] y
# > Which scope do you want to deploy to? Yamada
# > Link to existing project? [y/N] n
# > What's your project's name? portfolio
# > In which directory is your code located? ./
# > Want to override settings? [y/N] n
# 環境変数を追加(本番用)
pnpm dlx vercel env add SLACK_WEBHOOK_URL production
# 本番デプロイ
pnpm dlx vercel --prod
7.2 独自ドメイン設定
| 項目 | 推奨 | 備考 |
|---|---|---|
| ドメイン | your-name.dev / your-name.me | 年額1,500〜3,000円 |
| レジストラ | Cloudflare Registrar / Google Domains | 更新料が原価レベル |
| SSL | Vercel自動発行(Let’s Encrypt) | 追加料金なし |
| DNS | VercelのNameserverに委任 or A/CNAME | 初心者はNameserver委任が楽 |
7.3 デプロイ後にやる5項目
| 項目 | ツール | 合格ライン |
|---|---|---|
| Lighthouseスコア確認 | Chrome DevTools | Performance 90+ / SEO 100 |
| Core Web Vitals | PageSpeed Insights | LCP < 2.5s / CLS < 0.1 |
| OGP表示確認 | OGP確認ツール | Twitter/Discord/Slackで表示 |
| Search Console登録 | Google Search Console | sitemap送信&インデックス確認 |
| GA4設置 | Google Analytics 4 | リアルタイム計測でアクセス確認 |
パフォーマンス最適化の詳細はReactパフォーマンス最適化完全ガイド、モダンビルドツール&Webパフォーマンス最適化完全ガイドを参照してください。
8. GitHub整備〜採用担当者が見るのは結局ここ〜
8.1 GitHubプロフィールの整備
| 項目 | 整備内容 | 採用評価 |
|---|---|---|
| アイコン | 本人写真 or 統一感あるイラスト | 「中身もちゃんとしてそう」 |
| 名前 | 本名 or 名乗っている名義に統一 | 同一人物だとすぐ分かる |
| Bio | 「肩書き + 技術スタック + 一言」 | 1行で職種が伝わる |
| プロフィールREADME | username/usernameリポジトリを作成 | 「動的に見せられる人」 |
| Pinned Repositories | 代表作6本まで厳選 | 選別眼の証明 |
| Contributions(草) | 週2〜3コミット以上の頻度 | 継続力の証明 |
8.2 プロフィールREADMEの最小サンプル
### Hi, I'm Yamada Taro 👋
Frontend Engineer / 都内のSaaS企業勤務(週5フルリモート)
**現在の興味**
- Next.js App Router + RSC
- 大規模TypeScript(モノレポ・型エラー解消)
- アクセシビリティ(WAI-ARIA・キーボード操作)
**技術スタック**
TypeScript / React / Next.js / Tailwind / Node.js / Prisma / PostgreSQL
**外部発信**
- 📝 Blog: https://example.com
- 🐦 X: https://x.com/yamada
- 💼 Portfolio: https://yamada.dev
8.3 各リポジトリの整備チェックリスト
| 項目 | 具体内容 | 所要時間 |
|---|---|---|
| Description | 「1行で何のサービスか」 | 1分 |
| Topics | nextjs / typescript / tailwindcss など5個 | 2分 |
| Website | デプロイ済みURLを設定 | 1分 |
| README | 本記事の9〜10章を参照 | 30分〜2時間 |
| LICENSE | MIT が無難 | 3分 |
| .gitignore | 言語別の標準テンプレ | 1分 |
| ブランチ保護 | mainに直push禁止・PR必須 | 3分 |
9. READMEの書き方〜採用担当者の30秒で全部伝える〜
9.1 「読まれるREADME」の基本構造
READMEは「上から3分の1で勝負が決まる」と思ってください。最初に画面の半分以下しか見ない採用担当者は多く、その範囲に「何を解決するアプリで、誰が作って、何の技術を使っているか」を全部詰めるのが鉄則です。
9.2 推奨セクション順序
| 順番 | セクション | 長さの目安 |
|---|---|---|
| 1 | タイトル + バッジ | 1行 |
| 2 | 概要(1段落) | 3行 |
| 3 | デモGIF or スクショ | 1〜3枚 |
| 4 | 主な機能 | 箇条書き5〜8項目 |
| 5 | 技術スタック | 表 or バッジ |
| 6 | アーキテクチャ図 | 1枚 |
| 7 | ローカル起動手順 | 5〜10行のコード |
| 8 | 工夫した点 | 3項目 × 100字 |
| 9 | 苦労した点と解決法 | 2項目 × 100字 |
| 10 | 今後の改善予定 | 箇条書き3〜5項目 |
10. READMEテンプレート(コピペで使える)
# 📚 Reading Shelf



読書記録を本棚UIで管理できるパーソナルSaaSアプリ。
「読み終わった本を一覧で並べたい」「家族と本を共有したい」という個人の困りごとから生まれました。
🔗 **Demo**: https://reading-shelf.example.com
🔗 **記事**: https://yamada.dev/works/reading-shelf

## ✨ 主な機能
- ISBNコードから本を1秒で登録(Google Books API連携)
- 本棚UIで「読了/積読/読書中」を視覚的に管理
- 家族メンバーと本棚を共有(招待リンク発行)
- 月別の読了数・ジャンル傾向をグラフ表示
- 検索エンジンに公開しない「秘密の棚」機能
## 🛠 技術スタック
| 領域 | 技術 |
|----------------|---------------------------------------|
| フレームワーク | Next.js 15 (App Router) + RSC |
| 言語 | TypeScript (strict) |
| スタイリング | Tailwind CSS 4 + shadcn/ui |
| DB | PostgreSQL (Supabase) |
| ORM | Prisma 6 |
| 認証 | Auth.js v5 (Google / GitHub) |
| ホスティング | Vercel (Frontend) + Supabase (DB) |
| テスト | Vitest + Playwright |
| CI/CD | GitHub Actions |
## 🏗 アーキテクチャ

## 🚀 ローカル起動
```bash
git clone https://github.com/yamada/reading-shelf.git
cd reading-shelf
pnpm install
cp .env.example .env.local # <-- 編集
pnpm prisma migrate dev
pnpm dev
```
## 💡 工夫した点
- **Server Actions主体の実装**: JavaScript無効でも本の登録が動作するよう、useActionStateとprogressive enhancementを徹底
- **Prismaトランザクション**: 共有本棚の招待リンク発行時、トークン生成と権限作成を1トランザクションで担保
- **テストカバレッジ80%**: 主要導線(ログイン→登録→共有)はPlaywrightでE2Eテスト化
## 😣 苦労した点
- Auth.js v5(beta)のドキュメント不足: Discordコミュニティで情報を集め、自作のヘルパー関数で解消
- Server Actionsのエラー処理: useActionStateの型推論が複雑で、ジェネリクスで自前のラッパーを用意
## 🔮 今後の改善予定
- [ ] バーコードカメラ読み取り(getUserMedia API)
- [ ] PWA化(オフライン対応)
- [ ] ダークモード
## 📄 License
MIT © Yamada Taro
11. アピールポイントの言語化〜面接で勝つための準備〜
11.1 「STAR法」でポートフォリオの作業を言語化
| 要素 | 意味 | 例 |
|---|---|---|
| S (Situation) | 背景・課題 | 家族と読書記録を共有したいが、既存サービスは個人前提だった |
| T (Task) | 自分のミッション | 1人で設計〜デプロイまで完遂・1ヶ月以内に公開 |
| A (Action) | 具体的に何をしたか | Next.js 15 App Router + Auth.jsで認証実装・Prismaで権限設計 |
| R (Result) | 成果 | 家族5人で実利用・Lighthouseスコア95・週次でアップデート継続中 |
11.2 数字で語ると説得力が3倍になる
| NG表現 | OK表現 |
|---|---|
| パフォーマンスを改善しました | LCPを4.2秒→1.1秒に短縮(画像最適化+RSC化) |
| テストをちゃんと書きました | 主要導線3本をPlaywrightでE2E化・カバレッジ82% |
| 使いやすいUIにしました | 家族5人ユーザーテストでタスク完了率100%・平均操作時間42秒 |
| SEO対策しました | sitemap.xml自動生成+Search Console登録で初週20PV/日 |
12. やってはいけない事〜採用担当者が即落とすNG集〜
12.1 ポートフォリオサイト本体のNG
| NG行動 | 採用担当者の本音 |
|---|---|
| 404やレイアウト崩れがある | 「本番で品質出せない人だ」 |
| SP対応していない | 「2026年でこれは無理」 |
| OGPが設定されてない | 「メタデータの意識ゼロ」 |
| ロード10秒以上 | 「最適化の知識がない」 |
| HTTPSになってない | 「セキュリティ意識疑う」 |
| テンプレ感が強すぎる | 「Bootstrapチュートリアル止まり」 |
12.2 GitHubのNG
| NG行動 | 採用担当者の本音 |
|---|---|
| コミットメッセージが「update」「fix」「a」 | 「業務でもこれをやる人」 |
| “Initial commit”だけで止まっている | 「完成してないものを見せられても」 |
| .envや秘密情報がコミットされている | 「セキュリティ事故予備軍」 |
| node_modulesがコミットされている | 「.gitignoreの基礎が無い」 |
| READMEが「Getting Started」のままTemplate | 「中身を作ってない」 |
| 大量のフォークリポジトリだけ | 「自分で何も作ってない」 |
12.3 コード自体のNG
| NG | 理由 |
|---|---|
| anyだらけ | TypeScript使う意味が無い |
| useEffectで無限ループ | 動作確認してない証明 |
| 巨大1ファイルコンポーネント | 分割の発想がない |
| console.logが大量に残っている | 提出前に見直してない |
| 未使用のimport | ESLint設定していない |
| コメントゼロ or 過剰コメント | 意図を伝える設計力が低い |
Reactのアンチパターン全般はReactアンチパターン25選と回避策に詳しくまとめています。TypeScriptのよくあるエラーはTypeScriptよくあるエラーTOP30と解決法も参照してください。
13. 各社別ポートフォリオ評価軸〜企業タイプで見られる場所が違う〜
13.1 企業タイプ別の評価重点
| 企業タイプ | 最重視ポイント | 避けるべき題材 |
|---|---|---|
| Web系自社開発(メガベンチャー) | テスト・型・設計判断の言語化 | HTML/CSSだけのLP |
| Web系自社開発(スタートアップ) | スピード感・1人で全部やった感 | 過度にチュートリアル依存 |
| 受託開発(モダン) | 多様な技術スタック・README品質 | 同じフレームワークの作品ばかり |
| SES・SI系 | 業務系UIを作れる証明 | カラフルすぎる派手なUI |
| ゲーム系 | Unity/Unreal or WebGL作品 | 業務系SaaSのみ |
| 外資テック | 英語README・OSSコントリビュート | 日本語のみ・国内サービスのみ |
13.2 大手転職エージェントが薦める応募戦略
| エージェント | 特徴 | ポートフォリオの活かし方 |
|---|---|---|
| レバテックキャリア | IT全般特化・年収UP重視 | 技術スタックを表で整理して提出 |
| Geekly | Web/ゲーム特化・スピード対応 | 1ヶ月で動かす前提で完成度を高める |
| type転職エージェントIT | 都市部Web系の非公開求人多数 | 面接時に画面共有で実演 |
| マイナビIT AGENT | 20代未経験~初級者向け | 「自走できる」証拠として強調 |
各エージェントの詳細はレバテック完全レビュー2026、Geekly完全レビュー2026を参照してください。スクール検討中ならプログラミングスクール完全比較2026、テックアカデミー完全レビュー2026、侍エンジニア完全レビュー2026、DMM WEBCAMP完全レビュー2026でカリキュラム比較ができます。
14. ChatGPT/Copilot活用しすぎNG〜「AIに作らせた感」は秒でバレる〜
14.1 採用担当者がAI生成を見抜くポイント
| 見抜きポイント | 典型例 |
|---|---|
| 過剰に丁寧なコメント | 「// この関数はユーザーの名前を取得します」が全関数に |
| 不自然な変数名 | userDataObject / handleClickButtonForSubmit |
| 使ってないimport / 未参照変数 | AIが推測で書いた残骸 |
| 古いAPIの混在 | componentDidMountとuseEffectが同じファイルに |
| READMEと実装の乖離 | 「OAuth実装済み」と書いてあるのに該当ファイル無し |
| コミット粒度が極端 | 1コミットで300ファイル変更 |
14.2 AIの正しい使い方
AIは2026年時点で「設計の壁打ち相手」「ドキュメント検索の代替」「テストコードの叩き台」として使うのが正解です。「全部書かせる」のではなく、「自分が書いたコードをレビューさせる」方が遥かに学習効率も高く、面接でも語れる経験になります。「Copilotで〇〇を実装し、後でレビューと型強化を自分で行った」という説明は採用評価としてプラスです。
14.3 面接でAI活用を聞かれたときの模範回答
面接官「AIはどう使っていますか?」
OK回答(評価される)
「設計の段階で『この機能はServer ActionsとAPI Routesどちらが向くか』を
ChatGPTに整理させて、選択の根拠を言語化してから実装に入っています。
コード生成は使いますが、生成後は必ず型を強化して、未使用変数を除去し、
コミット前にrm -rf node_modules && pnpm install で再現性も確認しています。」
NG回答(落とされる)
「ChatGPTで全部書いてもらってます。便利ですよ。」
15. FAQ〜よく聞かれる質問に全部答える〜
Q1. ポートフォリオは何個作れば良いですか?
A. ポートフォリオサイト本体1つ + 制作物2〜3個が最低ラインで、これだけで未経験〜2年目の応募者の中でも上位30%に入れます。中堅以上の場合はOSSコントリビュート1件+業務効率化ツール1件などを加えると説得力が増します。
Q2. 期間はどのくらい掛けるのが普通ですか?
A. ポートフォリオサイト本体は1週間(土日+平日夜)で初版が公開可能です。制作物は1本あたり2〜4週間が目安です。完成度を上げる方が優先で、3ヶ月延々と作り続けるより1ヶ月で公開して継続更新する方が好印象です。
Q3. デザインに自信がないのですが大丈夫?
A. エンジニア応募であれば、shadcn/ui + Tailwindの組み合わせで十分です。デザイナーレベルを求められません。逆に「自分で頑張ったオリジナルデザインで微妙な見た目」より、「シンプルで整っている標準デザイン」の方が高評価です。
Q4. 既存サービスのクローンでもOKですか?
A. クローン+独自要素であればOKです。「Twitterクローン」だけだと差別化できませんが、「Twitterクローン+全投稿の感情分析を自動付与」のように+αがあれば題材として成立します。チュートリアル丸コピーだとバレるので、必ず自分なりの拡張を1つ以上加えましょう。
Q5. ポートフォリオ作りながら学習も必要です。両立のコツは?
A. 学習と制作を分離せず、「作りながら学ぶ」のが最短ルートです。例えばReact学習中ならReact Hooks完全実践ガイドを読みながら、実際にポートフォリオ内のSkillsセクションをuseStateとuseEffectで実装する、という形で読書と手を動かすを連動させましょう。
Q6. バックエンド未経験ですが必要ですか?
A. フロントエンド志望ならServer Actions + Prisma程度で十分です。Next.js App Routerの世界では、フロントエンド寄りのエンジニアでもDB読み書きや認証を触ることが多いため、その範囲はカバーしておきましょう。バックエンド志望ならExpress 5完全実践ガイド、NestJS完全実践ガイド、Hono完全実践ガイドを参照してください。
Q7. ポートフォリオを公開した後、放置で大丈夫?
A. NGです。月1〜2回はコミット or 機能追加を入れましょう。Vercel上の最終デプロイ日が3ヶ月以上前だと「もう作ってない人」と判断されます。逆に直近1ヶ月以内にコミットがあれば「現在進行形で技術を磨いている人」と高評価です。
Q8. 副業案件獲得でもポートフォリオは有効?
A. 転職以上に効きます。クラウドソーシングやエージェント経由の副業では、ポートフォリオの有無で単価が1.5〜2倍変わるのが実感です。特に「過去の制作物+デプロイ済みURL」を提出できると、書類選考なしで案件アサインされるケースもあります。
16. まとめ〜ポートフォリオは「完成」より「公開して更新し続ける」〜
ポートフォリオサイトは、エンジニアにとって名刺・カタログ・スキル証明書を1つに統合した最強の武器です。本記事では、Next.js 15 + Tailwind CSS 4を使った具体的な実装、READMEテンプレート、採用担当者の評価軸、やってはいけないNG、そしてAI活用の正しい線引きまで一気通貫で解説しました。
最後にお伝えしたいのは、「完成度100%を目指して非公開のまま3ヶ月過ごす」より「70%で公開して翌週から改善する」方が、転職市場では圧倒的に評価されるということです。コミット履歴・継続的な改善は、技術力そのものよりも「この人と仕事したい」という信頼に直結します。本記事を読み終わったら、まず create-next-app だけ叩いて、最低限のHelloワールドを公開してください。そこから始まる継続更新が、半年後のキャリアを変えます。

コメント