エンジニアポートフォリオ作成完全ガイド〜Next.js/Tailwind/採用担当者の見るポイント【2026年版】〜

「エンジニアのポートフォリオって、結局何を作れば評価されるの?」「Next.jsで作るのが今は正解?」「採用担当者って、実際にコードのどこを見てるの?」——本記事は、未経験〜中堅エンジニアが転職・案件獲得で武器になるポートフォリオサイトの設計から実装、デプロイ、評価される書き方までを、現役エンジニア(週5フルリモート/年収900万超/エンジニア採用面接100名以上経験)の視点で完全網羅したものです。コード例25個・チェックリスト・採用担当者の評価軸・やってはいけない事まで、6,000字超で徹底解説します。

本記事は、ポートフォリオを「とりあえず作って公開する」レベルから「Web系自社開発企業の選考通過率を3倍にする」レベルに引き上げるための実践ガイドです。Next.js 15 + Tailwind CSS 4を使った具体的な実装コードを15本以上掲載し、README・GitHub整備・SEO・Vercelデプロイまで一気通貫で解説します。なお、転職活動の流れ自体はフロントエンドエンジニア学習ロードマップ2026プログラミングスクール完全比較2026でカバーしていますので、合わせて読むと体系が完成します。

5秒サマリー(忙しい人向け)
必須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. 1. ポートフォリオの重要性〜採用担当者の視点で全部書く〜
    1. 1.1 なぜ職務経歴書だけでは不十分なのか
    2. 1.2 ポートフォリオが転職活動で果たす役割
    3. 1.3 採用担当者が30秒で見る場所
    4. 1.4 評価される人と落ちる人の決定的な違い
  2. 2. 何を作るか〜レベル別の題材選定と差別化戦略〜
    1. 2.1 レベル別の推奨題材
    2. 2.2 「自分の困りごと」起点で選ぶべき理由
    3. 2.3 ポートフォリオサイト本体と「制作物」は別物
  3. 3. 必要な要素〜技術スタック・コード品質・README〜
    1. 3.1 2026年時点の標準技術スタック
    2. 3.2 コード品質を担保する設定一覧
    3. 3.3 READMEに最低限書くべき7項目
  4. 4. Next.js 15 + Tailwind でポートフォリオサイト作成
    1. 4.1 プロジェクト初期化
    2. 4.2 ディレクトリ構成
    3. 4.3 ルートレイアウト(layout.tsx)
  5. 5. 実装〜Header/Hero/Skills/Works/Contact〜
    1. 5.1 Headerコンポーネント
    2. 5.2 Heroセクション
    3. 5.3 Skillsセクション
    4. 5.4 Worksセクション(制作物カード一覧)
    5. 5.5 制作物詳細ページ
    6. 5.6 Contact(Server Actions)
  6. 6. SEO対策〜ポートフォリオでも検索流入を意識する〜
    1. 6.1 動的メタデータ生成
    2. 6.2 sitemap.xmlとrobots.txt
    3. 6.3 OGP画像の動的生成
  7. 7. デプロイ〜Vercelに上げて初公開〜
    1. 7.1 Vercelへのデプロイ手順
    2. 7.2 独自ドメイン設定
    3. 7.3 デプロイ後にやる5項目
  8. 8. GitHub整備〜採用担当者が見るのは結局ここ〜
    1. 8.1 GitHubプロフィールの整備
    2. 8.2 プロフィールREADMEの最小サンプル
    3. 8.3 各リポジトリの整備チェックリスト
  9. 9. READMEの書き方〜採用担当者の30秒で全部伝える〜
    1. 9.1 「読まれるREADME」の基本構造
    2. 9.2 推奨セクション順序
  10. 10. READMEテンプレート(コピペで使える)
  11. 11. アピールポイントの言語化〜面接で勝つための準備〜
    1. 11.1 「STAR法」でポートフォリオの作業を言語化
    2. 11.2 数字で語ると説得力が3倍になる
  12. 12. やってはいけない事〜採用担当者が即落とすNG集〜
    1. 12.1 ポートフォリオサイト本体のNG
    2. 12.2 GitHubのNG
    3. 12.3 コード自体のNG
  13. 13. 各社別ポートフォリオ評価軸〜企業タイプで見られる場所が違う〜
    1. 13.1 企業タイプ別の評価重点
    2. 13.2 大手転職エージェントが薦める応募戦略
  14. 14. ChatGPT/Copilot活用しすぎNG〜「AIに作らせた感」は秒でバレる〜
    1. 14.1 採用担当者がAI生成を見抜くポイント
    2. 14.2 AIの正しい使い方
    3. 14.3 面接でAI活用を聞かれたときの模範回答
  15. 15. FAQ〜よく聞かれる質問に全部答える〜
    1. Q1. ポートフォリオは何個作れば良いですか?
    2. Q2. 期間はどのくらい掛けるのが普通ですか?
    3. Q3. デザインに自信がないのですが大丈夫?
    4. Q4. 既存サービスのクローンでもOKですか?
    5. Q5. ポートフォリオ作りながら学習も必要です。両立のコツは?
    6. Q6. バックエンド未経験ですが必要ですか?
    7. Q7. ポートフォリオを公開した後、放置で大丈夫?
    8. Q8. 副業案件獲得でもポートフォリオは有効?
  16. 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 15Astro / SvelteKit採用企業の利用率が圧倒的
言語TypeScriptJavaScript2026年は型なしは減点対象
スタイリングTailwind CSS 4CSS Modules / Panda CSSshadcn/uiとの相性が良い
UIコンポーネントshadcn/uiRadix UI / Mantineカスタマイズしやすい
状態管理Zustand / TanStack QueryJotai / Redux Toolkit軽量・モダン
フォームReact Hook Form + ZodConform型安全と相性抜群
DB(必要時)PostgreSQL + PrismaSQLite + Drizzle本番運用に近い
認証(必要時)Auth.js v5 (NextAuth)Clerk / Lucia無料&拡張性高い
デプロイVercelCloudflare PagesNext.jsとの相性が最強
CI/CDGitHub ActionsCircleCIGitHub完結で楽
テストVitest + React Testing LibraryJest + PlaywrightESM時代の標準

関連解説はNext.js 15完全実践ガイドTypeScript型推論完全ガイドReact Testing Library完全実践ガイドを参照してください。

3.2 コード品質を担保する設定一覧

ツール役割採用面接での評価
ESLint 9 (Flat Config)コード規約の自動チェックあれば「設定できる人」と判定
Prettier 3フォーマット統一無いと「読みづらい」と感じられる
Husky + lint-stagedコミット前の自動チェック「チーム開発意識あり」と高評価
tsconfig strict: true型の厳格化無いと「TypeScript分かってない」
Vitestユニットテストあると即「中堅以上」判定
Storybook 8UIカタログあればフロント職で大幅加点

これらの設定方法は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更新料が原価レベル
SSLVercel自動発行(Let’s Encrypt)追加料金なし
DNSVercelのNameserverに委任 or A/CNAME初心者はNameserver委任が楽

7.3 デプロイ後にやる5項目

項目ツール合格ライン
Lighthouseスコア確認Chrome DevToolsPerformance 90+ / SEO 100
Core Web VitalsPageSpeed InsightsLCP < 2.5s / CLS < 0.1
OGP表示確認OGP確認ツールTwitter/Discord/Slackで表示
Search Console登録Google Search Consolesitemap送信&インデックス確認
GA4設置Google Analytics 4リアルタイム計測でアクセス確認

パフォーマンス最適化の詳細はReactパフォーマンス最適化完全ガイドモダンビルドツール&Webパフォーマンス最適化完全ガイドを参照してください。

8. GitHub整備〜採用担当者が見るのは結局ここ〜

8.1 GitHubプロフィールの整備

項目整備内容採用評価
アイコン本人写真 or 統一感あるイラスト「中身もちゃんとしてそう」
名前本名 or 名乗っている名義に統一同一人物だとすぐ分かる
Bio「肩書き + 技術スタック + 一言」1行で職種が伝わる
プロフィールREADMEusername/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分
Topicsnextjs / typescript / tailwindcss など5個2分
Websiteデプロイ済みURLを設定1分
README本記事の9〜10章を参照30分〜2時間
LICENSEMIT が無難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

![Next.js](https://img.shields.io/badge/Next.js-15-black)
![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue)
![License](https://img.shields.io/badge/license-MIT-green)

読書記録を本棚UIで管理できるパーソナルSaaSアプリ。
「読み終わった本を一覧で並べたい」「家族と本を共有したい」という個人の困りごとから生まれました。

🔗 **Demo**: https://reading-shelf.example.com
🔗 **記事**: https://yamada.dev/works/reading-shelf

![demo](./docs/demo.gif)

## ✨ 主な機能

- 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                        |

## 🏗 アーキテクチャ

![architecture](./docs/architecture.png)

## 🚀 ローカル起動

```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が大量に残っている提出前に見直してない
未使用のimportESLint設定していない
コメントゼロ 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重視技術スタックを表で整理して提出
GeeklyWeb/ゲーム特化・スピード対応1ヶ月で動かす前提で完成度を高める
type転職エージェントIT都市部Web系の非公開求人多数面接時に画面共有で実演
マイナビIT AGENT20代未経験~初級者向け「自走できる」証拠として強調

各エージェントの詳細はレバテック完全レビュー2026Geekly完全レビュー2026を参照してください。スクール検討中ならプログラミングスクール完全比較2026テックアカデミー完全レビュー2026侍エンジニア完全レビュー2026DMM 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ワールドを公開してください。そこから始まる継続更新が、半年後のキャリアを変えます。

コメント

タイトルとURLをコピーしました