Lecture 1Next.jsプロジェクト作成 — ブログの骨格を作る

12:00

Next.jsプロジェクト作成 — ブログの骨格を作る

このコースで作るもの

このコースでは、Claude Code(AnthropicのAIコーディングCLIツール)を活用して、Next.jsとMarkdownベースのモダンなブログサイトをゼロから構築します。完成形は以下のような機能を持つブログです。

  • Markdownファイルで記事を管理(CMSなしで運用可能)
  • カテゴリ・タグによる記事分類
  • レスポンシブデザイン(Tailwind CSS)
  • OGP・SEO対策済み
  • RSSフィード対応
  • Vercelへの無料デプロイ

この第1講では、Claude Codeを使ってNext.jsプロジェクトを作成し、開発サーバーを起動するところまで進めます。従来なら公式ドキュメントを読みながら30分かかる作業が、Claude Codeなら数分で完了します。

前提条件と環境準備

プロジェクトを始める前に、以下のツールがインストールされていることを確認してください。

ツール バージョン 確認コマンド
Node.js 18.x 以上 node --version
npm 9.x 以上 npm --version
Claude Code 最新版 claude --version
Git 任意 git --version

Claude Codeをまだインストールしていない場合は、ターミナルで以下を実行します。

npm install -g @anthropic-ai/claude-code

インストール後、claude コマンドで起動し、Anthropicアカウントで認証を完了してください。Claude Codeは月額$20のProプランまたはAPI利用で動作します。

Claude Codeでプロジェクトを作成する

それでは実際にClaude Codeを使ってプロジェクトを作成しましょう。まずターミナルを開き、プロジェクトを作りたいディレクトリに移動します。

mkdir ~/projects && cd ~/projects
claude

Claude Codeが起動したら、以下のプロンプトを入力します。

> Next.jsのブログプロジェクトを作成して。
  プロジェクト名は "my-blog" で、以下の設定にして:
  - App Router を使用
  - TypeScript を有効化
  - Tailwind CSS を有効化
  - src/ ディレクトリを使用
  - ESLint を有効化

Claude Codeは create-next-app コマンドを実行し、自動的にプロジェクトを生成します。内部的には以下のコマンドが実行されます。

npx create-next-app@latest my-blog \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --use-npm

生成されたプロジェクト構造を確認しましょう。

my-blog/
├── src/
   └── app/
       ├── layout.tsx       ルートレイアウト
       ├── page.tsx         トップページ
       ├── globals.css      グローバルCSSTailwind
       └── favicon.ico
├── public/                  静的ファイル
├── next.config.ts           Next.js設定
├── tailwind.config.ts       Tailwind設定
├── tsconfig.json            TypeScript設定
├── package.json
└── .gitignore

App Routerの基本を理解する

Next.jsには2つのルーティング方式があります。

  • Pages Router(旧方式): pages/ ディレクトリにファイルを配置
  • App Router(新方式): app/ ディレクトリにファイルを配置

このコースではApp Routerを使います。App Routerの最大の特徴はReact Server Components(RSC)をデフォルトで使うことです。コンポーネントはサーバー側でレンダリングされるため、クライアントに送るJavaScriptの量が減り、パフォーマンスが向上します。

App Routerでの重要なファイル規約は以下のとおりです。

ファイル名 役割
page.tsx そのルートのページコンポーネント
layout.tsx そのルート以下の共通レイアウト
loading.tsx ローディング表示
error.tsx エラー表示
not-found.tsx 404ページ

Claude Codeに初期ファイルの内容を確認してもらいましょう。

> src/app/layout.tsx と page.tsx の内容を表示して、
  それぞれの役割を説明して

layout.tsx はアプリケーション全体を囲むルートレイアウトです。<html><body> タグを定義し、すべてのページに共通するUI要素(ヘッダー、フッターなど)を配置します。

// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "My Blog",
  description: "Next.jsで作ったブログ",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

開発サーバーの起動と確認

Claude Codeでサーバーを起動します。

> 開発サーバーを起動して

Claude Codeは以下を実行します。

cd my-blog && npm run dev

ブラウザで http://localhost:3000 を開くと、Next.jsのデフォルトページが表示されます。これでプロジェクトの骨格が完成しました。

次に、トップページをブログ用にカスタマイズしましょう。Claude Codeに以下を依頼します。

> src/app/page.tsx を、ブログのトップページ用にシンプルに書き換えて。
  My Blog」というタイトルと、「Next.jsで作ったブログです」という
  説明文だけのシンプルな内容にして

Claude Codeが生成するコードの例です。

// src/app/page.tsx
export default function Home() {
  return (
    <main className="max-w-4xl mx-auto px-4 py-16">
      <h1 className="text-4xl font-bold mb-4">My Blog</h1>
      <p className="text-gray-600 text-lg">
        Next.jsで作ったブログです
      </p>
    </main>
  );
}

ブラウザをリロードすると、シンプルなブログトップページが表示されます。Hot Module Replacement(HMR)により、ファイルを保存すると自動的にブラウザに反映されます。

演習問題

  1. プロジェクト作成: Claude Codeを使って、自分のブログプロジェクトを作成してください。プロジェクト名は自由に決めてかまいません。

  2. 構造の確認: Claude Codeに プロジェクトのディレクトリ構造を表示して と依頼し、生成されたファイル一覧を確認してください。

  3. ページの追加: Claude Codeに src/app/about/page.tsx を作成して。自己紹介ページにして と依頼し、http://localhost:3000/about でアクセスできることを確認してください。App Routerではフォルダ名がそのままURLパスになることを体感しましょう。

  4. メタデータの変更: layout.tsxmetadata オブジェクトを編集して、ブログのタイトルと説明文を自分のブログに合った内容に変更してください。

参考資料

Lecture 2レイアウト設計 — ヘッダーとフッターを作る

12:00

レイアウト設計 — ヘッダーとフッターを作る

レイアウトコンポーネントの役割

ブログサイトには「どのページにも共通して表示される要素」があります。ヘッダー(ナビゲーション)とフッター(コピーライト表記など)がその代表です。Next.js App Routerでは layout.tsx がこの共通要素を管理します。

レイアウトの仕組みを図で表すと以下のようになります。

layout.tsx共通部分
├── <Header />
├── {children}      ここにページ内容が入る
   ├── /  page.tsxトップページ
   ├── /about  about/page.tsx
   └── /blog/[slug]  blog/[slug]/page.tsx
└── <Footer />

App Routerのレイアウトには重要な特徴があります。レイアウトはページ遷移時に再レンダリングされません。つまりヘッダーやフッターはページを切り替えても状態が保持され、パフォーマンスに優れます。

Claude Codeでヘッダーコンポーネントを作成する

まずコンポーネントを格納するディレクトリを作り、ヘッダーを作成しましょう。Claude Codeに以下を依頼します。

> ブログのヘッダーコンポーネントを作成して。
  src/components/Header.tsx に配置して。
  - 左側にブログ名「My Blog」(リンクでトップページへ)
  - 右側にナビゲーション(ホーム、About、カテゴリ)
  - レスポンシブ対応(モバイルではハンバーガーメニュー)
  - Tailwind CSSでスタイリング

Claude Codeが生成するヘッダーコンポーネントの例です。

// src/components/Header.tsx
"use client";

import Link from "next/link";
import { useState } from "react";

export default function Header() {
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  const navItems = [
    { href: "/", label: "ホーム" },
    { href: "/about", label: "About" },
    { href: "/categories", label: "カテゴリ" },
  ];

  return (
    <header className="border-b border-gray-200 bg-white">
      <div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
        <Link href="/" className="text-xl font-bold text-gray-900">
          My Blog
        </Link>

        {/* デスクトップナビゲーション */}
        <nav className="hidden md:flex gap-6">
          {navItems.map((item) => (
            <Link
              key={item.href}
              href={item.href}
              className="text-gray-600 hover:text-gray-900 transition-colors"
            >
              {item.label}
            </Link>
          ))}
        </nav>

        {/* モバイルメニューボタン */}
        <button
          className="md:hidden p-2"
          onClick={() => setIsMenuOpen(!isMenuOpen)}
          aria-label="メニューを開く"
        >
          <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            {isMenuOpen ? (
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
            ) : (
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
            )}
          </svg>
        </button>
      </div>

      {/* モバイルメニュー */}
      {isMenuOpen && (
        <nav className="md:hidden border-t border-gray-200 px-4 py-2">
          {navItems.map((item) => (
            <Link
              key={item.href}
              href={item.href}
              className="block py-2 text-gray-600 hover:text-gray-900"
              onClick={() => setIsMenuOpen(false)}
            >
              {item.label}
            </Link>
          ))}
        </nav>
      )}
    </header>
  );
}

ここで注目してほしいのが、ファイルの先頭にある "use client" ディレクティブです。App Routerではすべてのコンポーネントがデフォルトでサーバーコンポーネントです。しかし useState のようなReactフック(クライアント側で状態管理する機能)を使う場合は "use client" を宣言してクライアントコンポーネントにする必要があります。

フッターコンポーネントの作成

続いてフッターを作ります。

> ブログのフッターコンポーネントを作成して。
  src/components/Footer.tsx に配置して。
  - コピーライト表記(年を動的に取得)
  - SNSリンク(Twitter、GitHub)
  - シンプルで控えめなデザイン

Claude Codeが生成するフッターの例です。

// src/components/Footer.tsx
export default function Footer() {
  return (
    <footer className="border-t border-gray-200 bg-gray-50">
      <div className="max-w-4xl mx-auto px-4 py-8">
        <div className="flex flex-col md:flex-row items-center justify-between gap-4">
          <p className="text-sm text-gray-500">
            &copy; {new Date().getFullYear()} My Blog. All rights reserved.
          </p>
          <div className="flex gap-4">
            <a
              href="https://twitter.com"
              target="_blank"
              rel="noopener noreferrer"
              className="text-gray-400 hover:text-gray-600 transition-colors"
            >
              Twitter
            </a>
            <a
              href="https://github.com"
              target="_blank"
              rel="noopener noreferrer"
              className="text-gray-400 hover:text-gray-600 transition-colors"
            >
              GitHub
            </a>
          </div>
        </div>
      </div>
    </footer>
  );
}

フッターは状態を持たないため "use client" は不要です。サーバーコンポーネントとして動作し、クライアントに送られるJavaScriptの量を削減できます。new Date().getFullYear() はサーバー側で実行されるため、つねに正しい年が表示されます。

レイアウトにコンポーネントを組み込む

ヘッダーとフッターを layout.tsx に組み込みましょう。

> src/app/layout.tsx を更新して、HeaderとFooterコンポーネントを
  組み込んで。mainタグで children を囲み、min-h-screenで
  フッターを画面下部に固定して

Claude Codeが更新した layout.tsx の例です。

// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Header from "@/components/Header";
import Footer from "@/components/Footer";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: {
    default: "My Blog",
    template: "%s | My Blog",
  },
  description: "Next.jsで作ったブログです",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className={`${inter.className} flex flex-col min-h-screen`}>
        <Header />
        <main className="flex-1">{children}</main>
        <Footer />
      </body>
    </html>
  );
}

flex flex-col min-h-screenflex-1 の組み合わせにより、コンテンツが少ないページでもフッターが画面最下部に固定されます。これは「Sticky Footer」と呼ばれる定番のレイアウトパターンです。

@/components/Header@ はパスエイリアスで、src/ ディレクトリを指します。tsconfig.json に自動で設定されているため、深いフォルダ構造でも ../../../ のような相対パスを書かずに済みます。

フォントの最適化

上記のコードでは next/font/google から Inter フォントを読み込んでいます。Next.jsのフォント最適化機能には以下の利点があります。

  • ビルド時にフォントをダウンロードして静的アセット化(Google Fontsへの外部リクエスト不要)
  • CLS(Cumulative Layout Shift)の防止
  • プライバシーの向上(Google Fontsのサーバーにリクエストが飛ばない)

日本語フォントを使いたい場合は、以下のようにClaude Codeに依頼できます。

> Noto Sans JP フォントを追加して、日本語テキストに適用して

演習問題

  1. ヘッダーのカスタマイズ: ブログ名を自分のブログの名前に変更し、ナビゲーション項目を自由に追加してみてください。

  2. フッターの拡張: Claude Codeに フッターにサイトマップリンクを追加して と依頼し、「プライバシーポリシー」「お問い合わせ」などのリンクを追加してみてください。

  3. Aboutページの作成: Claude Codeに src/app/about/page.tsx を作成して、自己紹介ページにして と依頼し、ヘッダーのナビゲーションから遷移できることを確認してください。

  4. アクティブリンクの実装: Claude Codeに 現在のページに対応するナビゲーションリンクを太字にして と依頼し、usePathname フックを使った実装を体験してください。

参考資料

Lecture 3Markdown記事管理 — 記事ファイルの構造を作る

12:00

Markdown記事管理 — 記事ファイルの構造を作る

なぜMarkdownで記事を管理するのか

ブログの記事管理には大きく3つの方式があります。

方式 代表例 メリット デメリット
CMS WordPress, microCMS GUIで編集、非エンジニアも使える サーバー/API依存、コスト
Markdown Gatsby, Hugo, Next.js Gitで管理、高速、無料 技術者向け
データベース 自作CMS 柔軟性が高い 開発・運用コスト大

このコースではMarkdownファイルで記事を管理する方式を採用します。理由は以下のとおりです。

  • Git管理:記事の変更履歴をGitで完全に追跡できる
  • ポータビリティ:プレーンテキストなので、どんなエディタでも編集可能
  • パフォーマンス:ビルド時にHTMLに変換するため、ランタイムのDB接続が不要
  • コスト:外部APIやデータベースが不要で、Vercelの無料枠で運用可能

エンジニアのブログは特にこの方式と相性がよく、Markdownでコードブロックも自然に書けます。

記事ファイルの構造を設計する

Claude Codeに記事管理のディレクトリ構造を作ってもらいましょう。

> ブログ記事をMarkdownファイルで管理するための構造を作成して。
  content/posts/ ディレクトリにMarkdownファイルを配置する方式で、
  サンプル記事を3つ作成して。
  各記事にはfrontmatter(タイトル、日付、カテゴリ、抜粋)を含めて

Claude Codeが作成するディレクトリ構造は以下のようになります。

my-blog/
├── content/
│   └── posts/
│       ├── hello-world.md
│       ├── nextjs-tutorial.md
│       └── markdown-guide.md
├── src/
│   └── ...
└── ...

サンプル記事の例を見てみましょう。

---
title: "はじめてのブログ記事"
date: "2026-01-15"
category: "日記"
excerpt: "Next.jsとMarkdownでブログを作り始めました。"
---

## ブログを始めました

Next.jsとMarkdownを使って、ブログを作り始めました。

技術的なメモや日々の学びを記録していきます。

## なぜ自作ブログなのか

既存のブログサービスもありますが、自分でカスタマイズできる
ブログを持つことで、Web開発のスキルも同時に磨けます。

--- で囲まれた部分がfrontmatter(フロントマター)です。YAML形式でメタデータを記述し、記事の属性を管理します。

gray-matterでfrontmatterを解析する

frontmatterを解析するために gray-matter ライブラリを使います。Claude Codeにインストールしてもらいましょう。

> gray-matter パッケージをインストールして。
  ブログ記事のfrontmatterを解析するために使う

Claude Codeが実行するコマンドです。

npm install gray-matter

次に、Markdownファイルを読み取るユーティリティ関数を作成します。

> src/lib/posts.ts を作成して。
  content/posts/ ディレクトリからMarkdownファイルを読み取り、
  frontmatterを解析する関数を実装して。
  以下の関数を用意して:
  - getAllPosts() — 全記事を日付降順で取得
  - getPostBySlug(slug) — スラッグで1記事を取得
  - getAllSlugs() — 全スラッグの一覧を取得

Claude Codeが生成する posts.ts の例です。

// src/lib/posts.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";

const postsDirectory = path.join(process.cwd(), "content/posts");

export interface Post {
  slug: string;
  title: string;
  date: string;
  category: string;
  excerpt: string;
  content: string;
}

export function getAllPosts(): Post[] {
  const fileNames = fs.readdirSync(postsDirectory);

  const posts = fileNames
    .filter((fileName) => fileName.endsWith(".md"))
    .map((fileName) => {
      const slug = fileName.replace(/\.md$/, "");
      const fullPath = path.join(postsDirectory, fileName);
      const fileContents = fs.readFileSync(fullPath, "utf8");
      const { data, content } = matter(fileContents);

      return {
        slug,
        title: data.title,
        date: data.date,
        category: data.category,
        excerpt: data.excerpt,
        content,
      };
    });

  return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
}

export function getPostBySlug(slug: string): Post | undefined {
  const fullPath = path.join(postsDirectory, `${slug}.md`);

  if (!fs.existsSync(fullPath)) {
    return undefined;
  }

  const fileContents = fs.readFileSync(fullPath, "utf8");
  const { data, content } = matter(fileContents);

  return {
    slug,
    title: data.title,
    date: data.date,
    category: data.category,
    excerpt: data.excerpt,
    content,
  };
}

export function getAllSlugs(): string[] {
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames
    .filter((fileName) => fileName.endsWith(".md"))
    .map((fileName) => fileName.replace(/\.md$/, ""));
}

この関数群はすべてサーバー側でのみ実行されます。fs(ファイルシステム)モジュールを使用しているため、ブラウザでは動作しません。App Routerのサーバーコンポーネントからこれらの関数を呼び出すことで、ビルド時またはリクエスト時にMarkdownファイルを読み取れます。

型安全な記事管理

TypeScriptを使っているため、Post インターフェースで記事の型を定義しています。これにより以下のメリットがあります。

  • エディタでの自動補完(post. と打つと候補が表示される)
  • frontmatterの記述漏れを検出
  • コンポーネントへのprops受け渡し時の型チェック

Claude Codeに型をさらに厳密にすることもできます。

> Post インターフェースに、tags フィールド(string配列)と
  published フィールド(boolean)を追加して。
  publishedがfalseの記事はgetAllPostsで除外して

このように、開発中に「こうしたい」と思ったらすぐにClaude Codeに依頼して機能を追加できるのが、AI駆動開発の大きな利点です。

ファイル命名規則とスラッグ

Markdownファイルのファイル名がそのままスラッグ(URLのパス部分)になります。命名には以下の規則を推奨します。

  • 英数字とハイフンのみ使用(例: hello-world.md
  • 日本語のファイル名は避ける(URLエンコーディングの問題)
  • 短くわかりやすい名前にする
  • 日付はfrontmatterで管理し、ファイル名には含めない
良い例:   hello-world.md  /blog/hello-world
良い例:   nextjs-tutorial.md  /blog/nextjs-tutorial
悪い例:   2026-01-15-最初の投稿.md(日本語・日付が冗長)

演習問題

  1. サンプル記事の追加: content/posts/ に自分で新しいMarkdownファイルを3つ以上作成してください。frontmatterにはtitle、date、category、excerptを必ず含めてください。

  2. カテゴリの設計: 自分のブログで使うカテゴリを5つ決めて、それぞれのカテゴリに1つずつサンプル記事を作成してください。

  3. 関数のテスト: Claude Codeに getAllPosts関数の動作を確認するための簡単なスクリプトを作って と依頼し、記事が正しく読み込まれることを検証してください。

  4. frontmatterの拡張: Claude Codeに frontmatterにtags(配列)とthumbnail(画像パス)フィールドを追加して、Postインターフェースも更新して と依頼してみてください。

参考資料

Lecture 4記事一覧ページ — トップページに記事を並べる

12:00

記事一覧ページ — トップページに記事を並べる

記事一覧表示の全体設計

前回の講義で getAllPosts() 関数を作成しました。この講義では、その関数を使ってトップページに記事の一覧を表示します。完成イメージは以下のような構成です。

トップページ
├── ページタイトル・説明文
└── 記事カードの一覧(日付降順)
    ├── カード1: タイトル / 日付 / カテゴリ / 抜粋
    ├── カード2: タイトル / 日付 / カテゴリ / 抜粋
    └── ...

App Routerのサーバーコンポーネントでは、コンポーネント内で直接 async/await やファイル読み取り処理を呼び出せます。データベースやAPIを使わずとも、ビルド時にMarkdownファイルを読み込んでHTMLを生成する静的サイト生成(SSG)が実現できます。

記事カードコンポーネントを作る

まず、個々の記事を表示するカードコンポーネントを作りましょう。

> 記事カードコンポーネントを作成して。
  src/components/PostCard.tsx に配置して。
  - タイトル(記事詳細へのリンク)
  - 公開日(読みやすい日本語形式)
  - カテゴリバッジ
  - 抜粋文
  - Tailwind CSSで角丸カードデザイン
  - ホバーで影が大きくなるアニメーション

Claude Codeが生成するコンポーネントの例です。

// src/components/PostCard.tsx
import Link from "next/link";
import type { Post } from "@/lib/posts";

interface PostCardProps {
  post: Post;
}

export default function PostCard({ post }: PostCardProps) {
  const formattedDate = new Date(post.date).toLocaleDateString("ja-JP", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });

  return (
    <article className="border border-gray-200 rounded-lg p-6 hover:shadow-lg transition-shadow duration-200">
      <div className="flex items-center gap-3 mb-3">
        <time className="text-sm text-gray-500" dateTime={post.date}>
          {formattedDate}
        </time>
        <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
          {post.category}
        </span>
      </div>

      <h2 className="text-xl font-bold mb-2">
        <Link
          href={`/blog/${post.slug}`}
          className="text-gray-900 hover:text-blue-600 transition-colors"
        >
          {post.title}
        </Link>
      </h2>

      <p className="text-gray-600 line-clamp-2">{post.excerpt}</p>

      <Link
        href={`/blog/${post.slug}`}
        className="inline-block mt-4 text-sm text-blue-600 hover:text-blue-800"
      >
        続きを読む &rarr;
      </Link>
    </article>
  );
}

いくつかのポイントを解説します。

  • <time> タグ: HTML5のセマンティック要素で、dateTime 属性に機械可読な日付を指定します。SEOやアクセシビリティに有利です。
  • line-clamp-2: Tailwind CSSのクラスで、テキストを2行に制限し、超過分を ... で省略します。
  • transition-shadow: ホバー時に影がスムーズに変化するアニメーションです。

トップページに記事一覧を表示する

記事カードを使って、トップページを構築しましょう。

> src/app/page.tsx を更新して、記事一覧ページにして。
  getAllPosts()で記事を取得し、PostCardコンポーネントで
  一覧表示して。記事がない場合のメッセージも表示して

Claude Codeが生成するページの例です。

// src/app/page.tsx
import { getAllPosts } from "@/lib/posts";
import PostCard from "@/components/PostCard";

export default function Home() {
  const posts = getAllPosts();

  return (
    <div className="max-w-4xl mx-auto px-4 py-12">
      <section className="mb-12">
        <h1 className="text-3xl font-bold mb-4">My Blog</h1>
        <p className="text-gray-600 text-lg">
          技術メモと日々の学びを記録するブログです
        </p>
      </section>

      <section>
        <h2 className="text-2xl font-bold mb-6">最新の記事</h2>

        {posts.length === 0 ? (
          <p className="text-gray-500">まだ記事がありません</p>
        ) : (
          <div className="grid gap-6">
            {posts.map((post) => (
              <PostCard key={post.slug} post={post} />
            ))}
          </div>
        )}
      </section>
    </div>
  );
}

注目すべき点は、このコンポーネントに async キーワードが不要なことです。getAllPosts()fs.readFileSync を使った同期関数であり、サーバーコンポーネント内で直接呼び出せます。useEffect でデータをフェッチする必要もありません。

これがApp Routerの大きな利点です。サーバーサイドでデータ取得とレンダリングを行うため、クライアント側のJavaScriptが最小限になり、ページの初期表示が高速になります。

日付のソートと表示形式

getAllPosts() は既に日付降順でソートされた記事を返しますが、表示形式についても考慮しましょう。日付の表示には Intl.DateTimeFormat を使うのがモダンな方法です。

// 簡潔な表示
new Date("2026-01-15").toLocaleDateString("ja-JP");
// → "2026/1/15"

// 詳細な表示
new Date("2026-01-15").toLocaleDateString("ja-JP", {
  year: "numeric",
  month: "long",
  day: "numeric",
});
// → "2026年1月15日"

// 相対時間の表示(「3日前」など)
const rtf = new Intl.RelativeTimeFormat("ja", { numeric: "auto" });
rtf.format(-3, "day");
// → "3日前"

Claude Codeにさらに高度な日付処理を依頼することもできます。

> 記事の日付を「3日前」「1ヶ月前」のような相対的な表示にして。
  ただし1ヶ月以上前の記事は「2026年1月15日」形式で表示して

グリッドレイアウトのバリエーション

記事一覧のレイアウトには複数のパターンがあります。Claude Codeにレイアウトの変更を依頼してみましょう。

> 記事一覧を2カラムのグリッドにして。
  最新の1記事だけ全幅で大きく表示して

Claude Codeが生成するレイアウトの例です。

<div className="space-y-6">
  {/* 最新記事を大きく表示 */}
  {posts.length > 0 && (
    <PostCard post={posts[0]} featured />
  )}

  {/* 残りの記事を2カラムで表示 */}
  <div className="grid md:grid-cols-2 gap-6">
    {posts.slice(1).map((post) => (
      <PostCard key={post.slug} post={post} />
    ))}
  </div>
</div>

md:grid-cols-2 はTailwind CSSのレスポンシブクラスで、768px以上の画面幅で2カラムになり、それ以下では1カラムに戻ります。モバイルファーストの設計が簡潔に実現できます。

演習問題

  1. 記事の追加と確認: content/posts/ に5つ以上の記事を作成し、トップページに正しく表示されることを確認してください。異なるカテゴリと日付を設定してみましょう。

  2. レイアウトの変更: Claude Codeに 記事一覧を3カラムのグリッドにして と依頼し、レスポンシブデザインを確認してください。ブラウザの幅を変えて表示の変化を観察しましょう。

  3. ページネーション: Claude Codeに 記事が10件以上あるときに、ページネーション(1ページ5件)を追加して と依頼してみてください。

  4. 検索機能の追加: Claude Codeに 記事タイトルで検索できる検索バーをトップページに追加して と依頼し、クライアントコンポーネントによるフィルタリングを体験してください。

参考資料

Lecture 5記事詳細ページ — 動的ルーティングで記事を表示する

12:00

記事詳細ページ — 動的ルーティングで記事を表示する

動的ルーティングとは

ブログには記事ごとに固有のURLが必要です。たとえば /blog/hello-world/blog/nextjs-tutorial のように、記事のスラッグに応じてページを動的に生成します。

Next.js App Routerでは、フォルダ名を角括弧 [slug] で囲むことで動的ルーティングを実現します。

src/app/
├── page.tsx            /
├── about/page.tsx      /about
└── blog/
    └── [slug]/
        └── page.tsx    /blog/hello-world, /blog/nextjs-tutorial, ...

[slug] はプレースホルダーで、URLのパス部分がコンポーネントに params として渡されます。記事が100件あっても、page.tsx は1つだけです。

MarkdownをHTMLに変換するライブラリ

記事詳細ページでは、Markdownの本文をHTMLに変換して表示する必要があります。remarkrehype というライブラリ群を使います。

> 以下のパッケージをインストールして。
  Markdownの記事をHTMLに変換するために使う:
  - remark
  - remark-html
  - remark-gfm(GitHub Flavored Markdown対応)
  - rehype-highlight(コードのシンタックスハイライト)

Claude Codeが実行するコマンドです。

npm install remark remark-html remark-gfm rehype-highlight

各ライブラリの役割を整理します。

ライブラリ 役割
remark Markdownをパースして抽象構文木(AST)に変換
remark-html Markdown ASTをHTMLに変換
remark-gfm テーブル、打ち消し線、タスクリスト等に対応
rehype-highlight コードブロックにシンタックスハイライトを適用

Markdownの変換パイプラインは以下のような流れです。

Markdown テキスト
  → remark(パース) → MDAST(Markdown AST)
    → remark-gfm(拡張構文)
      → remark-html(HTML変換) → HTML文字列

記事詳細ページを作成する

Claude Codeに記事詳細ページの作成を依頼しましょう。

> 記事詳細ページを作成して
  src/app/blog/[slug]/page.tsx に配置して
  - URLのスラッグから記事を取得
  - Markdownをremark+remark-gfmでHTMLに変換して表示
  - タイトル日付カテゴリも表示
  - 記事が見つからない場合は404ページ
  - generateStaticParams で全記事の静的ページを生成
  - generateMetadata で記事ごとのメタデータを設定

Claude Codeが生成するページの例です。

// src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getPostBySlug, getAllSlugs } from "@/lib/posts";
import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkHtml from "remark-html";
import type { Metadata } from "next";

interface Props {
  params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
  const slugs = getAllSlugs();
  return slugs.map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    return { title: "記事が見つかりません" };
  }

  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    notFound();
  }

  const processedContent = await remark()
    .use(remarkGfm)
    .use(remarkHtml)
    .process(post.content);

  const contentHtml = processedContent.toString();

  const formattedDate = new Date(post.date).toLocaleDateString("ja-JP", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });

  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <header className="mb-8">
        <div className="flex items-center gap-3 mb-4">
          <time className="text-sm text-gray-500" dateTime={post.date}>
            {formattedDate}
          </time>
          <span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
            {post.category}
          </span>
        </div>
        <h1 className="text-3xl font-bold text-gray-900">
          {post.title}
        </h1>
      </header>

      <div
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: contentHtml }}
      />
    </article>
  );
}

重要な概念の解説

generateStaticParams

generateStaticParams は、ビルド時にどのスラッグのページを生成するかをNext.jsに伝える関数です。この関数が返すパラメータの配列に基づいて、静的HTMLが事前生成されます。

export async function generateStaticParams() {
  const slugs = getAllSlugs();
  // [{ slug: "hello-world" }, { slug: "nextjs-tutorial" }, ...]
  return slugs.map((slug) => ({ slug }));
}

これにより npm run build 時に全記事のHTMLが生成され、配信時にサーバーサイドの処理が不要になります。CDNからの高速配信が可能です。

generateMetadata

generateMetadata は、ページごとに異なるメタデータ(タイトル、説明文)を設定する関数です。layout.tsxtemplate: "%s | My Blog" と設定していれば、各記事のタイトルは「記事タイトル | My Blog」となります。

dangerouslySetInnerHTML

Reactでは通常、HTMLを直接挿入することは推奨されません(XSS攻撃のリスクがあるため)。しかしMarkdownから変換したHTMLは信頼できるソースなので、dangerouslySetInnerHTML を使います。名前が「dangerous」なのは、開発者に「このHTMLの安全性を確認していますか?」と注意を促すためです。

proseクラス

prose は Tailwind CSS Typography プラグインのクラスです。Markdownから変換された素のHTMLに美しいタイポグラフィスタイルを適用します。見出し、段落、リスト、コードブロック、テーブルなどが自動的にスタイリングされます。

> @tailwindcss/typography プラグインをインストールして
  tailwind.config.ts に追加して
npm install @tailwindcss/typography
// tailwind.config.ts
import type { Config } from "tailwindcss";

const config: Config = {
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
  plugins: [require("@tailwindcss/typography")],
};

export default config;

404ページのカスタマイズ

存在しないスラッグにアクセスした場合、notFound() 関数が呼ばれます。カスタムの404ページを作りましょう。

> src/app/blog/[slug]/not-found.tsx を作成して
  記事が見つかりませんというメッセージと
  トップページへのリンクを表示して
// src/app/blog/[slug]/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="max-w-3xl mx-auto px-4 py-24 text-center">
      <h1 className="text-4xl font-bold mb-4">記事が見つかりません</h1>
      <p className="text-gray-600 mb-8">
        お探しの記事は存在しないか削除された可能性があります
      </p>
      <Link
        href="/"
        className="text-blue-600 hover:text-blue-800 underline"
      >
        トップページに戻る
      </Link>
    </div>
  );
}

演習問題

  1. 記事詳細の確認: content/posts/ に3つ以上の記事を作り、それぞれの記事詳細ページが正しく表示されることをブラウザで確認してください。

  2. Markdown機能のテスト: サンプル記事に見出し、リスト、コードブロック、テーブル、リンク、画像を含めて、すべて正しく表示されることを確認してください。

  3. 前後の記事リンク: Claude Codeに 記事詳細ページの下部に「前の記事」「次の記事」へのリンクを追加して と依頼してみてください。

  4. 目次の自動生成: Claude Codeに 記事の見出し(h2, h3)から自動で目次を生成して、記事の上部に表示して と依頼し、長い記事のナビゲーションを改善してください。

参考資料

Lecture 6カテゴリ機能 — 記事をジャンル別に分類する

12:00

カテゴリ機能 — 記事をジャンル別に分類する

カテゴリとタグの設計

ブログの記事が増えてくると、読者が目的の記事を見つけにくくなります。カテゴリとタグによる分類機能を追加して、ブログのユーザビリティを向上させましょう。

カテゴリとタグの使い分けは以下のとおりです。

項目 カテゴリ タグ
少数(5〜10個程度) 多数(自由に追加)
階層 1記事1カテゴリ 1記事に複数タグ
役割 大まかな分類 詳細なキーワード
「技術」「日記」「レビュー」 「Next.js」「React」「TypeScript」

まずfrontmatterに tags フィールドを追加する設計にしましょう。

---
title: "Next.jsでブログを作る"
date: "2026-01-20"
category: "技術"
tags: ["Next.js", "React", "TypeScript"]
excerpt: "Next.jsとMarkdownでブログを構築する方法を解説します。"
---

Claude Codeに、既存の Post インターフェースと関連関数を更新してもらいます。

> src/lib/posts.ts を更新して:
  1. Post インターフェースに tags: string[] を追加
  2. getAllCategories() 関数を追加(全カテゴリの一覧と記事数を返す)
  3. getAllTags() 関数を追加(全タグの一覧と記事数を返す)
  4. getPostsByCategory(category) 関数を追加
  5. getPostsByTag(tag) 関数を追加

Claude Codeが追加する関数の例です。

// src/lib/posts.ts に追加

export interface CategoryCount {
  name: string;
  count: number;
}

export function getAllCategories(): CategoryCount[] {
  const posts = getAllPosts();
  const categoryMap = new Map<string, number>();

  posts.forEach((post) => {
    const count = categoryMap.get(post.category) || 0;
    categoryMap.set(post.category, count + 1);
  });

  return Array.from(categoryMap.entries())
    .map(([name, count]) => ({ name, count }))
    .sort((a, b) => b.count - a.count);
}

export function getAllTags(): CategoryCount[] {
  const posts = getAllPosts();
  const tagMap = new Map<string, number>();

  posts.forEach((post) => {
    post.tags?.forEach((tag) => {
      const count = tagMap.get(tag) || 0;
      tagMap.set(tag, count + 1);
    });
  });

  return Array.from(tagMap.entries())
    .map(([name, count]) => ({ name, count }))
    .sort((a, b) => b.count - a.count);
}

export function getPostsByCategory(category: string): Post[] {
  return getAllPosts().filter((post) => post.category === category);
}

export function getPostsByTag(tag: string): Post[] {
  return getAllPosts().filter((post) => post.tags?.includes(tag));
}

カテゴリ一覧ページを作る

カテゴリの一覧を表示するページを作成しましょう。

> カテゴリ一覧ページを作成して。
  src/app/categories/page.tsx に配置して。
  - 全カテゴリを一覧表示
  - 各カテゴリの記事数を表示
  - カテゴリをクリックすると、そのカテゴリの記事一覧に遷移

Claude Codeが生成するページの例です。

// src/app/categories/page.tsx
import Link from "next/link";
import { getAllCategories } from "@/lib/posts";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "カテゴリ一覧",
  description: "ブログ記事のカテゴリ一覧です",
};

export default function CategoriesPage() {
  const categories = getAllCategories();

  return (
    <div className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-3xl font-bold mb-8">カテゴリ</h1>

      <div className="grid md:grid-cols-2 gap-4">
        {categories.map((category) => (
          <Link
            key={category.name}
            href={`/categories/${encodeURIComponent(category.name)}`}
            className="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-md transition-all"
          >
            <span className="text-lg font-medium">{category.name}</span>
            <span className="text-sm text-gray-500 bg-gray-100 px-3 py-1 rounded-full">
              {category.count}
            </span>
          </Link>
        ))}
      </div>
    </div>
  );
}

カテゴリ別記事一覧ページ

特定のカテゴリに属する記事だけを表示するページを作ります。ここでも動的ルーティングを使います。

> カテゴリ別の記事一覧ページを作成して
  src/app/categories/[category]/page.tsx に配置して
  - URLからカテゴリ名を取得
  - そのカテゴリの記事を一覧表示
  - generateStaticParams で全カテゴリのページを事前生成
  - カテゴリが存在しない場合は404

Claude Codeが生成するページの例です。

// src/app/categories/[category]/page.tsx
import { notFound } from "next/navigation";
import {
  getPostsByCategory,
  getAllCategories,
} from "@/lib/posts";
import PostCard from "@/components/PostCard";
import type { Metadata } from "next";

interface Props {
  params: Promise<{ category: string }>;
}

export async function generateStaticParams() {
  const categories = getAllCategories();
  return categories.map((cat) => ({
    category: cat.name,
  }));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { category } = await params;
  const decodedCategory = decodeURIComponent(category);
  return {
    title: `${decodedCategory}の記事一覧`,
    description: `カテゴリ「${decodedCategory}」の記事一覧です`,
  };
}

export default async function CategoryPage({ params }: Props) {
  const { category } = await params;
  const decodedCategory = decodeURIComponent(category);
  const posts = getPostsByCategory(decodedCategory);

  if (posts.length === 0) {
    notFound();
  }

  return (
    <div className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-3xl font-bold mb-2">
        {decodedCategory}
      </h1>
      <p className="text-gray-600 mb-8">{posts.length}件の記事</p>

      <div className="grid gap-6">
        {posts.map((post) => (
          <PostCard key={post.slug} post={post} />
        ))}
      </div>
    </div>
  );
}

日本語のカテゴリ名はURLエンコーディングされるため、decodeURIComponent でデコードする必要があります。たとえば「技術」というカテゴリは /categories/%E6%8A%80%E8%A1%93 のようなURLになります。

サイドバーにカテゴリリストを表示する

記事詳細ページやトップページのサイドバーにカテゴリ一覧を表示するコンポーネントを作りましょう。

> サイドバー用のカテゴリリストコンポーネントを作成して。
  src/components/CategoryList.tsx に配置して。
  - カテゴリ名と記事数を表示
  - 現在見ているカテゴリをハイライト
  - コンパクトなデザイン
// src/components/CategoryList.tsx
import Link from "next/link";
import { getAllCategories } from "@/lib/posts";

interface CategoryListProps {
  currentCategory?: string;
}

export default function CategoryList({ currentCategory }: CategoryListProps) {
  const categories = getAllCategories();

  return (
    <aside className="border border-gray-200 rounded-lg p-4">
      <h3 className="font-bold mb-3">カテゴリ</h3>
      <ul className="space-y-2">
        {categories.map((cat) => (
          <li key={cat.name}>
            <Link
              href={`/categories/${encodeURIComponent(cat.name)}`}
              className={`flex items-center justify-between text-sm ${
                currentCategory === cat.name
                  ? "text-blue-600 font-medium"
                  : "text-gray-600 hover:text-gray-900"
              }`}
            >
              <span>{cat.name}</span>
              <span className="text-gray-400">{cat.count}</span>
            </Link>
          </li>
        ))}
      </ul>
    </aside>
  );
}

このコンポーネントをトップページや記事詳細ページに組み込むことで、読者がカテゴリ間をスムーズに移動できるようになります。

タグクラウドの実装

タグの重要度(記事数)に応じてサイズを変えるタグクラウドも実装しましょう。

> タグクラウドコンポーネントを作成して。
  src/components/TagCloud.tsx に配置して。
  - 記事数が多いタグほど大きく表示
  - クリックでタグ別記事一覧に遷移
// src/components/TagCloud.tsx
import Link from "next/link";
import { getAllTags } from "@/lib/posts";

export default function TagCloud() {
  const tags = getAllTags();
  const maxCount = Math.max(...tags.map((t) => t.count));

  const getSize = (count: number) => {
    const ratio = count / maxCount;
    if (ratio > 0.7) return "text-xl font-bold";
    if (ratio > 0.4) return "text-base font-medium";
    return "text-sm";
  };

  return (
    <div className="flex flex-wrap gap-2">
      {tags.map((tag) => (
        <Link
          key={tag.name}
          href={`/tags/${encodeURIComponent(tag.name)}`}
          className={`${getSize(tag.count)} text-gray-600 hover:text-blue-600 bg-gray-100 hover:bg-blue-50 px-3 py-1 rounded-full transition-colors`}
        >
          {tag.name}
        </Link>
      ))}
    </div>
  );
}

演習問題

  1. カテゴリの設計: 自分のブログで使うカテゴリを5つ設定し、各カテゴリに最低2つの記事を作成してください。カテゴリ一覧ページとカテゴリ別ページの動作を確認しましょう。

  2. タグの追加: すべてのサンプル記事にタグを追加し、タグ別の記事一覧ページ(/tags/[tag]/page.tsx)をClaude Codeに作成してもらってください。

  3. パンくずリスト: Claude Codeに カテゴリ別ページにパンくずリスト(ホーム > カテゴリ > カテゴリ名)を追加して と依頼し、ナビゲーションを改善してください。

  4. サイドバーレイアウト: Claude Codeに トップページを2カラムにして、右サイドバーにカテゴリリストとタグクラウドを配置して と依頼し、情報量のあるレイアウトを試してください。

参考資料

Lecture 7デザイン調整 — Tailwind CSSで美しくする

12:00

デザイン調整 — Tailwind CSSで美しくする

デザインの方針を決める

ここまでの講義でブログの基本機能が完成しました。この講義では、Tailwind CSSを使ってブログ全体のデザインを洗練させます。目指すのは「読みやすく、シンプルで、プロフェッショナルな見た目」です。

デザイン改善の3つの柱を意識しましょう。

  • タイポグラフィ: フォント、行間、文字サイズの最適化
  • カラースキーム: 統一感のある色使い、ダークモード対応
  • レスポンシブ: モバイル、タブレット、デスクトップすべてで快適な表示

Typographyプラグインの活用

第5講でインストールした @tailwindcss/typography プラグインの prose クラスをさらに活用しましょう。

> 記事詳細ページのproseクラスをカスタマイズして。
  - リンクの色をブルーに
  - コードブロックの背景色をグレーに
  - 画像を角丸に
  - 見出しにアンカーリンクを追加
  tailwind.config.ts の typography セクションで設定して

Claude Codeが生成する設定の例です。

// tailwind.config.ts
import type { Config } from "tailwindcss";
import typography from "@tailwindcss/typography";

const config: Config = {
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
  darkMode: "class",
  theme: {
    extend: {
      typography: {
        DEFAULT: {
          css: {
            a: {
              color: "#2563eb",
              textDecoration: "underline",
              "&:hover": {
                color: "#1d4ed8",
              },
            },
            "code::before": { content: "" },
            "code::after": { content: "" },
            code: {
              backgroundColor: "#f3f4f6",
              padding: "0.2em 0.4em",
              borderRadius: "0.25rem",
              fontWeight: "400",
            },
            img: {
              borderRadius: "0.5rem",
            },
            h2: {
              borderBottom: "1px solid #e5e7eb",
              paddingBottom: "0.5rem",
            },
          },
        },
      },
    },
  },
  plugins: [typography],
};

export default config;

prose クラスの各要素をカスタマイズすることで、Markdownから生成されるHTMLの見た目を細かく制御できます。code::beforecode::aftercontent: "" は、デフォルトで付与されるバッククォート記号を非表示にするための設定です。

ダークモードの実装

現代のブログにはダークモードが欠かせません。Claude Codeに実装を依頼しましょう。

> ダークモードを実装して。
  - テーマ切り替えボタンをヘッダーに追加
  - ローカルストレージで設定を保存
  - システムの設定(prefers-color-scheme)に対応
  - 全コンポーネントのダークモード対応

まず、テーマ管理用のコンポーネントを作成します。

// src/components/ThemeToggle.tsx
"use client";

import { useEffect, useState } from "react";

export default function ThemeToggle() {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  useEffect(() => {
    const saved = localStorage.getItem("theme");
    if (saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
      setTheme("dark");
      document.documentElement.classList.add("dark");
    }
  }, []);

  const toggleTheme = () => {
    const next = theme === "light" ? "dark" : "light";
    setTheme(next);
    localStorage.setItem("theme", next);

    if (next === "dark") {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  };

  return (
    <button
      onClick={toggleTheme}
      className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
      aria-label="テーマを切り替える"
    >
      {theme === "light" ? (
        <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
            d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
        </svg>
      ) : (
        <svg className="w-5 h-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
            d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
        </svg>
      )}
    </button>
  );
}

各コンポーネントにダークモード対応のクラスを追加します。Tailwind CSSでは dark: プレフィックスを使います。

// ダークモード対応の例
<div className="bg-white dark:bg-gray-900">
  <h1 className="text-gray-900 dark:text-white">タイトル</h1>
  <p className="text-gray-600 dark:text-gray-300">テキスト</p>
  <div className="border-gray-200 dark:border-gray-700">
    カード
  </div>
</div>

レスポンシブデザインの強化

Tailwind CSSのブレークポイントを活用して、デバイスごとに最適なレイアウトを作りましょう。

ブレークポイント デバイス
デフォルト 0px〜 スマートフォン
sm: 640px〜 大きいスマートフォン
md: 768px〜 タブレット
lg: 1024px〜 デスクトップ
xl: 1280px〜 大画面
> トップページのレイアウトをレスポンシブ対応にして。
  - モバイル: 1カラム、カードは縦積み
  - タブレット: 2カラムグリッド
  - デスクトップ: メインコンテンツ + サイドバーの2カラム

Claude Codeが生成するレスポンシブレイアウトの例です。

<div className="max-w-6xl mx-auto px-4 py-12">
  <div className="lg:grid lg:grid-cols-3 lg:gap-8">
    {/* メインコンテンツ */}
    <div className="lg:col-span-2">
      <h2 className="text-2xl font-bold mb-6">最新の記事</h2>
      <div className="grid sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6">
        {posts.map((post) => (
          <PostCard key={post.slug} post={post} />
        ))}
      </div>
    </div>

    {/* サイドバー */}
    <aside className="mt-12 lg:mt-0 space-y-6">
      <CategoryList />
      <TagCloud />
    </aside>
  </div>
</div>

モバイルファーストの設計原則に従い、デフォルトのスタイル(何もプレフィックスがないクラス)はモバイル用です。sm:md:lg: でより大きな画面向けのスタイルを上書きしていきます。

アニメーションとトランジション

細かなアニメーションはサイトの印象を大きく変えます。Claude Codeにいくつかのアニメーションを追加してもらいましょう。

> 以下のアニメーションを追加して:
  - ページ遷移時にフェードインする効果
  - 記事カードのホバーで少し浮き上がる効果
  - スクロールで記事が順番に表示される効果
// ホバーで浮き上がる記事カード
<article className="border rounded-lg p-6
  hover:shadow-lg hover:-translate-y-1
  transition-all duration-200 ease-out">
  {/* ... */}
</article>

// フェードインアニメーション(CSS)
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.animate-fade-in {
  animation: fadeIn 0.5s ease-out;
}

hover:-translate-y-1 はホバー時に要素を4px上に移動させます。transition-all duration-200 と組み合わせることで、スムーズに浮き上がる効果が得られます。

カラーパレットの統一

ブログ全体の色に統一感を持たせるために、カスタムカラーを定義しましょう。

> tailwind.config.ts にブログ用のカスタムカラーを追加して。
  プライマリカラーはブルー系、アクセントカラーはアンバー系で
// tailwind.config.ts の theme.extend に追加
colors: {
  primary: {
    50: "#eff6ff",
    100: "#dbeafe",
    500: "#3b82f6",
    600: "#2563eb",
    700: "#1d4ed8",
  },
  accent: {
    50: "#fffbeb",
    100: "#fef3c7",
    500: "#f59e0b",
    600: "#d97706",
  },
},

これにより text-primary-600bg-accent-50 のようなクラスが使えるようになり、色の一貫性を保ちやすくなります。ブランドカラーを変更したくなった場合も、この設定ファイルを変更するだけで全体に反映されます。

演習問題

  1. ダークモードの実装: この講義の手順に従ってダークモードを実装し、すべてのページで正しく切り替わることを確認してください。

  2. カスタムカラー: 自分のブランドカラーを決めて tailwind.config.ts にカスタムカラーを定義し、ヘッダーやリンクに適用してください。

  3. フォントの変更: Claude Codeに 日本語フォントをNoto Sans JPに変更して と依頼し、フォントが変わることを確認してください。

  4. レスポンシブ確認: ブラウザの開発者ツール(F12)でレスポンシブモードを使い、iPhone、iPad、デスクトップの3サイズで表示を確認してください。崩れている箇所があればClaude Codeに修正を依頼しましょう。

参考資料

Lecture 8OGP・SEO対策 — SNSシェアと検索最適化

12:00

OGP・SEO対策 — SNSシェアと検索最適化

なぜSEO対策が重要なのか

せっかくブログを書いても、Googleの検索結果に表示されなければ誰にも読まれません。また、SNSでシェアされたときに魅力的なプレビューカードが表示されれば、クリック率が大幅に向上します。

Next.jsはSEO対策に非常に強いフレームワークです。サーバーサイドレンダリングにより、検索エンジンのクローラーが完全なHTMLを取得できます。さらにApp Routerには Metadata API が組み込まれており、ページごとのメタデータ管理が簡単です。

この講義でカバーする内容は以下のとおりです。

項目 目的
title / description Google検索結果に表示される情報
OGP (Open Graph Protocol) SNSシェア時のプレビューカード
Twitter Card Twitterでの表示形式
sitemap.xml 検索エンジンへのページ一覧の通知
robots.txt クローラーへの制御指示
構造化データ (JSON-LD) リッチリザルト(リッチスニペット)対応

Next.js Metadata APIの活用

App Routerでは metadata オブジェクトまたは generateMetadata 関数でメタデータを設定します。既にいくつかの講義で使ってきましたが、ここではさらに詳細に設定しましょう。

> src/app/layout.tsx のメタデータを充実させて。
  以下を設定して:
  - サイト全体のデフォルトOGP画像
  - Twitter Cardの設定
  - サイトのURL(metadataBase)
  - テンプレートを使ったtitle
  - favicon、apple-touch-icon

Claude Codeが生成する設定の例です。

// src/app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  metadataBase: new URL("https://my-blog.vercel.app"),
  title: {
    default: "My Blog",
    template: "%s | My Blog",
  },
  description: "技術メモと日々の学びを記録するブログです",
  openGraph: {
    title: "My Blog",
    description: "技術メモと日々の学びを記録するブログです",
    url: "https://my-blog.vercel.app",
    siteName: "My Blog",
    locale: "ja_JP",
    type: "website",
    images: [
      {
        url: "/og-default.png",
        width: 1200,
        height: 630,
        alt: "My Blog",
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    title: "My Blog",
    description: "技術メモと日々の学びを記録するブログです",
    creator: "@your_twitter",
    images: ["/og-default.png"],
  },
  icons: {
    icon: "/favicon.ico",
    apple: "/apple-touch-icon.png",
  },
  robots: {
    index: true,
    follow: true,
  },
};

metadataBase は相対パスの基準URLです。OGP画像のパスが /og-default.png の場合、実際のURLは https://my-blog.vercel.app/og-default.png に解決されます。

記事ごとのOGP設定

記事詳細ページでは generateMetadata を使って、記事ごとに異なるメタデータを設定します。

> src/app/blog/[slug]/page.tsx の generateMetadata を拡張して
  - 記事タイトルをOGPタイトルに
  - 記事の抜粋をdescriptionに
  - 記事ごとのOGP画像URLを設定将来の動的OGP画像生成に対応
  - 記事URLをcanonicalに設定
  - JSON-LD構造化データを追加

Claude Codeが生成する generateMetadata の例です。

// src/app/blog/[slug]/page.tsx

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = getPostBySlug(slug);

  if (!post) {
    return { title: "記事が見つかりません" };
  }

  const url = `https://my-blog.vercel.app/blog/${slug}`;

  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: url,
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url,
      type: "article",
      publishedTime: post.date,
      authors: ["My Blog"],
      tags: post.tags,
      images: [
        {
          url: `/api/og?title=${encodeURIComponent(post.title)}`,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [`/api/og?title=${encodeURIComponent(post.title)}`],
    },
  };
}

動的OGP画像の生成

Next.jsの ImageResponse を使って、記事タイトルからOGP画像を動的に生成できます。これにより、すべての記事に固有のシェア画像を自動で付与できます。

> 動的OGP画像を生成するAPIルートを作成して。
  src/app/api/og/route.tsx に配置して。
  - クエリパラメータからタイトルを取得
  - 1200x630のOGP画像を生成
  - ブログ名とタイトルを表示
  - グラデーション背景

Claude Codeが生成するOGP画像生成APIの例です。

// src/app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";

export const runtime = "edge";

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const title = searchParams.get("title") || "My Blog";

  return new ImageResponse(
    (
      <div
        style={{
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          justifyContent: "center",
          alignItems: "center",
          background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
          padding: "60px",
        }}
      >
        <div
          style={{
            fontSize: "48px",
            fontWeight: "bold",
            color: "white",
            textAlign: "center",
            lineHeight: 1.4,
            maxWidth: "900px",
          }}
        >
          {title}
        </div>
        <div
          style={{
            fontSize: "24px",
            color: "rgba(255,255,255,0.8)",
            marginTop: "30px",
          }}
        >
          My Blog
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  );
}

/api/og?title=Next.jsでブログを作る にアクセスすると、タイトル入りのOGP画像が動的に生成されます。runtime: "edge" を指定することで、Edge Runtimeで高速に画像を生成します。

sitemap.xmlの生成

Googleにブログの全ページを知らせるために、sitemap.xml を自動生成しましょう。

> sitemap.xml を自動生成する機能を追加して。
  src/app/sitemap.ts に配置して。
  - 全記事のURL
  - カテゴリページのURL
  - トップページ、Aboutページ
  - 各ページの最終更新日

Claude Codeが生成する sitemap.ts の例です。

// src/app/sitemap.ts
import { MetadataRoute } from "next";
import { getAllPosts, getAllCategories } from "@/lib/posts";

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = "https://my-blog.vercel.app";
  const posts = getAllPosts();
  const categories = getAllCategories();

  const postEntries = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.date),
    changeFrequency: "monthly" as const,
    priority: 0.8,
  }));

  const categoryEntries = categories.map((cat) => ({
    url: `${baseUrl}/categories/${encodeURIComponent(cat.name)}`,
    lastModified: new Date(),
    changeFrequency: "weekly" as const,
    priority: 0.5,
  }));

  return [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: "daily",
      priority: 1.0,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: "monthly",
      priority: 0.3,
    },
    ...postEntries,
    ...categoryEntries,
  ];
}

このファイルを配置するだけで、Next.jsが自動的に /sitemap.xml エンドポイントを生成します。Google Search Consoleにこのsitemapを登録すれば、クローラーが効率的にブログをインデックスします。

robots.txtの設定

> robots.txt を追加して。
  src/app/robots.ts に配置して
// src/app/robots.ts
import { MetadataRoute } from "next";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: "/api/",
    },
    sitemap: "https://my-blog.vercel.app/sitemap.xml",
  };
}

構造化データの追加

Google検索でリッチスニペット(著者名、公開日、画像など)を表示するために、JSON-LD形式の構造化データを記事ページに追加します。

> 記事詳細ページにJSON-LD構造化データ(Article型)を追加して。
  scriptタグでhead内に出力して
// 記事詳細ページに追加
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      "@context": "https://schema.org",
      "@type": "Article",
      headline: post.title,
      datePublished: post.date,
      author: {
        "@type": "Person",
        name: "My Blog Author",
      },
      description: post.excerpt,
    }),
  }}
/>

演習問題

  1. OGP確認: ブログをデプロイ後、OGP CheckerTwitter Card Validator でOGPが正しく設定されていることを確認してください。

  2. 動的OGP画像: /api/og?title=テスト にブラウザでアクセスし、OGP画像が生成されることを確認してください。フォントや色を変更してみましょう。

  3. sitemap確認: /sitemap.xml にアクセスして、全記事が含まれていることを確認してください。

  4. Google Search Console: ブログをVercelにデプロイ後、Google Search Consoleに登録し、sitemapを送信してみてください。

参考資料

Lecture 9RSSフィード — 更新通知機能を追加する

12:00

RSSフィード — 更新通知機能を追加する

RSSフィードとは

RSS(Really Simple Syndication)は、Webサイトの更新情報を配信するためのXML形式のフォーマットです。読者はRSSリーダー(Feedly、Inoreaderなど)にブログのRSSフィードを登録するだけで、新しい記事が公開されるたびに通知を受け取れます。

RSSフィードが重要な理由は以下のとおりです。

  • 読者のリテンション: 一度購読した読者が定期的に戻ってくる
  • SEOの間接的な効果: RSSリーダー経由のアクセスが増える
  • 他サービスとの連携: Zapier、IFTTTなどで「新記事公開 → Twitterに自動投稿」が可能
  • 技術ブログの定番: 開発者はRSSリーダーを使う割合が高い

RSSフィードのXMLは以下のような構造です。

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>My Blog</title>
    <link>https://my-blog.vercel.app</link>
    <description>技術メモと日々の学びを記録するブログ</description>
    <atom:link href="https://my-blog.vercel.app/feed.xml"
               rel="self" type="application/rss+xml"/>
    <item>
      <title>記事タイトル</title>
      <link>https://my-blog.vercel.app/blog/hello-world</link>
      <pubDate>Wed, 15 Jan 2026 00:00:00 GMT</pubDate>
      <description>記事の抜粋...</description>
      <guid>https://my-blog.vercel.app/blog/hello-world</guid>
    </item>
    <!-- 他の記事... -->
  </channel>
</rss>

Route Handlerでフィードを生成する

Next.js App Routerの Route Handler を使って、RSSフィードのエンドポイントを作成します。Route Handlerは route.ts ファイルに定義し、HTTPリクエストに応じたレスポンスを返す機能です。

まず、RSSフィード生成に便利なライブラリをインストールします。

> RSS フィード生成用のパッケージをインストールして。
  feed パッケージを使って
npm install feed

feed パッケージはRSS 2.0、Atom 1.0、JSON Feedの3形式に対応した人気のライブラリです。

RSSフィードのRoute Handlerを実装する

Claude Codeにフィード生成の実装を依頼しましょう。

> RSSフィードを生成するRoute Handlerを作成して。
  src/app/feed.xml/route.ts に配置して。
  - feedパッケージを使用
  - 全記事を含める
  - RSS 2.0形式
  - Content-Typeを正しく設定

Claude Codeが生成するRoute Handlerの例です。

// src/app/feed.xml/route.ts
import { Feed } from "feed";
import { getAllPosts } from "@/lib/posts";

export async function GET() {
  const posts = getAllPosts();
  const siteUrl = "https://my-blog.vercel.app";

  const feed = new Feed({
    title: "My Blog",
    description: "技術メモと日々の学びを記録するブログです",
    id: siteUrl,
    link: siteUrl,
    language: "ja",
    favicon: `${siteUrl}/favicon.ico`,
    copyright: `All rights reserved ${new Date().getFullYear()}, My Blog`,
    feedLinks: {
      rss2: `${siteUrl}/feed.xml`,
      atom: `${siteUrl}/atom.xml`,
    },
    author: {
      name: "My Blog Author",
      link: siteUrl,
    },
  });

  posts.forEach((post) => {
    feed.addItem({
      title: post.title,
      id: `${siteUrl}/blog/${post.slug}`,
      link: `${siteUrl}/blog/${post.slug}`,
      description: post.excerpt,
      content: post.content,
      date: new Date(post.date),
      category: post.category
        ? [{ name: post.category }]
        : undefined,
    });
  });

  return new Response(feed.rss2(), {
    headers: {
      "Content-Type": "application/xml; charset=utf-8",
      "Cache-Control": "s-maxage=3600, stale-while-revalidate",
    },
  });
}

/feed.xml にアクセスすると、RSS形式のXMLが返されます。Cache-Control ヘッダーにより、CDNで1時間キャッシュされ、古いキャッシュを返しながらバックグラウンドで新しいフィードを生成する設定にしています。

Atomフィードの追加

Atomフィードも同時に生成しましょう。feed パッケージは atom1() メソッドでAtom形式にも対応しています。

> Atomフィードも追加して。
  src/app/atom.xml/route.ts に配置して
// src/app/atom.xml/route.ts
import { Feed } from "feed";
import { getAllPosts } from "@/lib/posts";

export async function GET() {
  const posts = getAllPosts();
  const siteUrl = "https://my-blog.vercel.app";

  const feed = new Feed({
    title: "My Blog",
    description: "技術メモと日々の学びを記録するブログです",
    id: siteUrl,
    link: siteUrl,
    language: "ja",
    copyright: `All rights reserved ${new Date().getFullYear()}, My Blog`,
    feedLinks: {
      rss2: `${siteUrl}/feed.xml`,
      atom: `${siteUrl}/atom.xml`,
    },
    author: {
      name: "My Blog Author",
      link: siteUrl,
    },
  });

  posts.forEach((post) => {
    feed.addItem({
      title: post.title,
      id: `${siteUrl}/blog/${post.slug}`,
      link: `${siteUrl}/blog/${post.slug}`,
      description: post.excerpt,
      date: new Date(post.date),
    });
  });

  return new Response(feed.atom1(), {
    headers: {
      "Content-Type": "application/atom+xml; charset=utf-8",
      "Cache-Control": "s-maxage=3600, stale-while-revalidate",
    },
  });
}

HTMLヘッダーにフィードリンクを追加

RSSリーダーがフィードを自動検出できるよう、HTMLの <head> にリンクタグを追加します。

> layout.tsx のメタデータに、RSSフィードとAtomフィードの
  alternateリンクを追加して
// src/app/layout.tsx の metadata に追加
export const metadata: Metadata = {
  // ...既存の設定
  alternates: {
    types: {
      "application/rss+xml": "https://my-blog.vercel.app/feed.xml",
      "application/atom+xml": "https://my-blog.vercel.app/atom.xml",
    },
  },
};

これにより、RSSリーダーのブラウザ拡張機能(例: Feedbro、RSSHub Radar)がフィードを自動検出できるようになります。

フッターにRSSリンクを追加

読者がフィードを見つけやすいよう、フッターにRSSアイコンとリンクを追加しましょう。

> フッターにRSSフィードへのリンクを追加して。
  RSSアイコン(SVG)付きで
// Footer.tsx に追加
<a
  href="/feed.xml"
  target="_blank"
  rel="noopener noreferrer"
  className="text-gray-400 hover:text-orange-500 transition-colors"
  aria-label="RSSフィード"
>
  <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
    <path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1z"/>
  </svg>
</a>

フィードの検証とテスト

生成されたフィードが正しいか検証しましょう。

> 開発サーバーを起動して、/feed.xml にアクセスして
  XMLが正しく出力されているか確認して

ブラウザで http://localhost:3000/feed.xml にアクセスすると、XMLが表示されます。検証には以下のツールが便利です。

ツール URL 用途
W3C Feed Validation https://validator.w3.org/feed/ RSS/Atomの構文検証
Feed Viewer ブラウザ拡張機能 フィードのプレビュー

デプロイ後に上記のバリデーターでURLを入力し、エラーがないことを確認しましょう。特に日本語のエンコーディングや日付形式の問題が起きやすいので注意が必要です。

演習問題

  1. フィードの確認: 開発サーバーで /feed.xml/atom.xml にアクセスし、全記事が含まれていることを確認してください。

  2. RSSリーダーで購読: Feedly(https://feedly.com)にアカウントを作成し、デプロイ後のブログのRSSフィードを購読してみてください。

  3. JSON Feedの追加: Claude Codeに JSON Feed形式のフィードも追加して。/feed.json で配信して と依頼し、3つの形式でフィードを提供してみてください。

  4. フィードの内容拡張: Claude Codeに RSSフィードの各記事にMarkdownから変換したHTMLを含めて と依頼し、RSSリーダーで全文が読めるようにしてください。

参考資料

Lecture 10Vercelデプロイ — ブログを世界に公開する

12:00

Vercelデプロイ — ブログを世界に公開する

Vercelとは

Vercelは、Next.jsの開発元であるVercel社が提供するホスティングプラットフォームです。Next.jsアプリケーションのデプロイに最適化されており、以下の特徴があります。

特徴 説明
ゼロ設定デプロイ GitリポジトリをつなげるだけでCI/CDが完了
グローバルCDN 世界中のエッジサーバーから高速配信
自動HTTPS SSL証明書を自動で取得・更新
プレビューデプロイ プルリクエストごとにプレビュー環境を自動作成
無料枠 個人ブログなら無料で運用可能(Hobbyプラン)
サーバーレス関数 API RouteやRoute Handlerをサーバーレスで実行

個人ブログであればHobbyプラン(無料)で十分です。月間100GBの帯域幅、1日あたり100回のデプロイが含まれます。

デプロイ前のチェック

Claude Codeにデプロイ前の確認を依頼しましょう。

> デプロイ前のチェックをして。
  以下を確認して:
  1. npm run build が成功するか
  2. TypeScriptの型エラーがないか
  3. ESLintのエラーがないか
  4. 環境変数は必要か
  5. 画像などの静的ファイルが正しく配置されているか

Claude Codeが実行するコマンドです。

npm run build

ビルドが成功すると、以下のような出力が表示されます。

Route (app)                              Size     First Load JS
  /                                    5.2 kB        89.3 kB
  /about                               1.8 kB        85.9 kB
  /blog/[slug]                         3.4 kB        87.5 kB
  /categories                          2.1 kB        86.2 kB
  /categories/[category]              2.8 kB        86.9 kB
  /feed.xml                            0 B           0 B
  /sitemap.xml                         0 B           0 B

  (Static)   prerendered as static content
  (SSG)      prerendered as static HTML (uses generateStaticParams)

は静的ページ、generateStaticParams で事前生成されたページです。ブログの全ページが静的に生成されるため、CDNから直接配信でき、サーバーの負荷がかかりません。

GitリポジトリをGitHubにプッシュする

VercelはGitHubリポジトリと連携してデプロイします。まずプロジェクトをGitHubにプッシュしましょう。

> このプロジェクトをGitHubリポジトリにプッシュして。
  リポジトリ名は "my-blog" で、publicリポジトリとして作成して

Claude Codeが実行する手順です。

# .gitignore を確認(create-next-appで自動生成済み)
git init
git add .
git commit -m "Initial commit: Next.js blog with Markdown"

# GitHub CLIでリポジトリを作成してプッシュ
gh repo create my-blog --public --source=. --push

.gitignore には node_modules/.next/out/ などが含まれており、不要なファイルはリポジトリに含まれません。content/posts/ のMarkdownファイルはGitで管理されるため、記事の変更履歴もすべて追跡できます。

Vercelにデプロイする

Vercelへのデプロイ方法は2つあります。

方法1: Vercel Webサイトからデプロイ(推奨)

  1. vercel.com にアクセスし、GitHubアカウントでサインアップ
  2. 「New Project」をクリック
  3. GitHubリポジトリ「my-blog」をインポート
  4. 設定はデフォルトのまま「Deploy」をクリック
  5. 数分でデプロイ完了。https://my-blog.vercel.app のようなURLが割り当てられる

方法2: Vercel CLIからデプロイ

コマンドラインからもデプロイできます。

> Vercel CLIをインストールして、プロジェクトをデプロイして
# Vercel CLIのインストール
npm install -g vercel

# ログイン
vercel login

# デプロイ(初回はプロジェクト設定の対話式ウィザード)
vercel

# 本番環境にデプロイ
vercel --prod

初回の vercel コマンドではプロジェクトの設定を聞かれます。

? Set up and deploy "~/projects/my-blog"? [Y/n] y
? Which scope do you want to deploy to? Your Name
? Link to existing project? [y/N] n
? What's your project's name? my-blog
? In which directory is your code located? ./
? Want to modify these settings? [y/N] n

環境変数の設定

ブログに外部サービスのAPIキーが必要な場合は、VercelのダッシュボードまたはCLIで環境変数を設定します。

# CLIで環境変数を追加
vercel env add NEXT_PUBLIC_SITE_URL

# 値を入力
# production: https://my-blog.vercel.app

Next.jsでは NEXT_PUBLIC_ プレフィックスの付いた環境変数はクライアントサイドでもアクセス可能です。APIキーなど秘密にしたい値にはこのプレフィックスを付けないよう注意してください。

今回のブログは外部APIを使っていないため、環境変数は NEXT_PUBLIC_SITE_URL 程度で十分です。

カスタムドメインの設定

Vercelの無料プランでもカスタムドメインを設定できます。

手順:
1. ドメインを購入(お名前.comGoogle Domains等
2. Vercelダッシュボード  Settings  Domains
3. ドメインを入力して「Add
4. 表示されるDNSレコードをドメイン管理画面で設定
   - Aレコード: 76.76.21.21
   - CNAMEレコード: cname.vercel-dns.com
5. DNS反映を待つ(最大48時間、通常は数分〜数時間)
6. HTTPS証明書は自動で取得される

Claude CodeにメタデータのURLを更新してもらいましょう。

> metadataBase のURLをカスタムドメインに変更して。
  ドメインは my-blog.com で

継続的デプロイ(CI/CD)

VercelとGitHubの連携により、以下のワークフローが自動化されます。

記事を書く(Markdown)
  → git add → git commit → git push
    → Vercelが自動検知
      → npm run build を実行
        → 成功したら本番にデプロイ
        → 失敗したらデプロイ中止、エラー通知

プルリクエストを作成すると、Vercelはプレビューデプロイを自動で作成します。プレビューURLで変更を確認してからマージすれば、安全に本番環境を更新できます。

新しい記事を公開する手順は以下のとおりです。

# 記事を書く
# content/posts/new-article.md を作成

# Gitで管理
git add content/posts/new-article.md
git commit -m "Add: 新しい記事を公開"
git push

# → Vercelが自動デプロイ(1〜2分で完了)

デプロイ後の確認

デプロイが完了したら、以下を確認しましょう。

> デプロイ後のチェックリストを作って
  • ページ表示: トップページ、記事詳細、カテゴリページが正常に表示されるか
  • レスポンシブ: モバイルでも正しく表示されるか
  • OGP: SNSでURLをシェアして、プレビューカードが表示されるか
  • RSSフィード: /feed.xml にアクセスしてXMLが返されるか
  • sitemap: /sitemap.xml にアクセスして全ページが含まれるか
  • パフォーマンス: Lighthouse(Chrome DevTools)でスコアを確認
  • リンク切れ: 全ページのリンクが正しく動作するか

Lighthouseのスコアは、Next.jsの静的サイト生成を使っていれば、パフォーマンス90以上を達成できるはずです。

コースのまとめ

おめでとうございます。全10講を通じて、Claude Codeを使ったNext.jsブログの構築が完了しました。このコースで習得したスキルを振り返りましょう。

講義 習得スキル
1. プロジェクト作成 create-next-app, App Router, TypeScript
2. レイアウト設計 layout.tsx, コンポーネント分割, レスポンシブ
3. Markdown管理 gray-matter, frontmatter, ファイル操作
4. 記事一覧 サーバーコンポーネント, データ取得, カードUI
5. 記事詳細 動的ルーティング, remark/rehype, generateStaticParams
6. カテゴリ機能 フィルタリング, タグシステム, URLエンコーディング
7. デザイン調整 Typography, ダークモード, アニメーション
8. OGP・SEO Metadata API, sitemap, 構造化データ
9. RSSフィード Route Handler, feedパッケージ, XML生成
10. デプロイ Vercel, CI/CD, カスタムドメイン

演習問題

  1. デプロイ実行: この講義の手順に従って、実際にVercelにブログをデプロイしてください。デプロイURLを控えておきましょう。

  2. 記事の追加と自動デプロイ: 新しいMarkdownファイルを作成し、git push でVercelに自動デプロイされることを確認してください。

  3. Lighthouseスコアの測定: Chrome DevToolsのLighthouseタブで、パフォーマンス、アクセシビリティ、ベストプラクティス、SEOの4項目のスコアを測定してください。目標は全項目90以上です。

  4. 拡張機能の追加: Claude Codeを使って、以下のいずれかの機能を追加してみてください。

  5. コメント機能(giscus連携)
  6. 記事の検索機能
  7. 関連記事の表示
  8. ニュースレター購読フォーム

参考資料