🎯 この記事で得られるもの
- 開発時間を 90%削減する具体的な方法
- 4 つの AI ツール使い分け戦略(Claude Code + Cursor + Codex + Copilot)
- SSG + ヘッドレス CMSによる爆速サイト構築
- 実コード付き囲碁棋譜プレイヤーの実装
はじめに
「2 週間かかる開発を 3 日で完了」これは誇張ではありません。
私はもっぱら会社の業務で AI 駆動開発でインフラ〜フロントエンドまで対応しています。今回、息抜きに行きつけの浦添囲碁会館の Web サイトのリプレイスを Claude Code を中心に、Cursor、Codex、Copilot を組み合わせた AI ツール群で実践し、驚異的な成果を得ました。
※隔週の土曜囲碁大会に参加しています 😀
📊 定量的な成果(実測値)
指標 | 従来の開発 | AI 駆動開発 | 改善率 |
---|---|---|---|
開発期間 | 14〜21 日 | 3 日 | 85.7%短縮 |
コーディング時間 | 72 時間 | 8 時間 | 88.9%削減 |
Lighthouse Score | 50 | 98 | 96%向上 |
月額ホスティング費用 | 3,000 円〜 | 実質 100 円未満 | 97%削減 |
プロジェクト概要
沖縄県浦添市にある囲碁教室・囲碁会館の Web サイトです。地域の囲碁愛好家のためのコミュニティハブとして、教室情報、大会情報、棋譜記録などを提供しています。
🔄 ビフォーアフター
画像で見る劇的な変化
Before(旧サイト) | After(新サイト) |
---|---|
![]() |
![]() |
項目 | Before 🔴 | After 🟢 | 改善率 |
---|---|---|---|
レスポンシブ対応 | ❌ 非対応 | ✅ 完全対応 | – |
表示速度 | 🐌 3.5 秒 | ⚡ 0.8 秒 | 77%高速化 |
Lighthouse Score | 📊 50 | 📊 98 | 96%向上 |
モバイル対応 | ❌ 未対応 | ✅ 最適化 | – |
SEO 対策 | 🔻 基本のみ | 🔺 完全最適化 | – |
月額費用 | 💰 3,000 円〜 | 💸 100 円未満 | 97%削減 |
技術スタック
{
"フレームワーク": "Next.js 14.2 (App Router)",
"言語": "TypeScript 5.3",
"スタイリング": "Tailwind CSS 3.4",
"CMS": "ヘッドレスCMS (Firestore)",
"データベース": "Prisma 5.7 + PostgreSQL 16",
"認証": "Firebase Auth v10",
"ホスティング": "Firebase Hosting + CDN (月額100円未満)",
"ビルド": "SSG (Static Site Generation)",
"AI開発支援": "Claude Code + Cursor + Codex + Copilot",
"パフォーマンス": "Core Web Vitals最適化"
}
開発プロセス
1. 既存サイトの分析
このプロジェクトは、既存の Web サイトを最新技術でリプレイスする案件でした。最初のステップは、現行サイトの完全な理解から始まりました。
スクレイピングによるサイトマップ作成
私:「既存サイトの構造を把握したいので、スクレイピングしてサイトマップを作成してください」
Claude Code は、Puppeteer を使用して既存サイトをクロールし、SITEMAP.md を自動生成してくれました:
const puppeteer = require("puppeteer");
const fs = require("fs").promises;
async function crawlSite(baseUrl) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const visited = new Set();
const siteMap = [];
async function crawlPage(url, depth = 0) {
if (visited.has(url) || depth > 3) return;
visited.add(url);
try {
await page.goto(url, { waitUntil: "networkidle2" });
const pageInfo = await page.evaluate(() => ({
title: document.title,
description: document.querySelector('meta[name="description"]')
?.content,
h1: document.querySelector("h1")?.textContent,
sections: Array.from(document.querySelectorAll("h2")).map(
(h) => h.textContent
),
}));
siteMap.push({ url, depth, ...pageInfo });
const links = await page.evaluate(() =>
Array.from(document.querySelectorAll("a[href]"))
.map((a) => a.href)
.filter((href) => href.startsWith(window.location.origin))
);
for (const link of links) {
await crawlPage(link, depth + 1);
}
} catch (error) {
console.error(`Error crawling ${url}:`, error.message);
}
}
await crawlPage(baseUrl);
await browser.close();
return siteMap;
}
async function generateSiteMapDocument(siteMap) {
let markdown = "# サイトマップ\n\n";
const pagesByDepth = {};
siteMap.forEach((page) => {
if (!pagesByDepth[page.depth]) pagesByDepth[page.depth] = [];
pagesByDepth[page.depth].push(page);
});
Object.keys(pagesByDepth).forEach((depth) => {
const indent = " ".repeat(parseInt(depth));
pagesByDepth[depth].forEach((page) => {
markdown += `${indent}- [${page.title}](${page.url})\n`;
if (page.sections?.length > 0) {
page.sections.forEach((section) => {
markdown += `${indent} - ${section}\n`;
});
}
});
});
await fs.writeFile("SITEMAP.md", markdown);
}
ペアプログラミングでの要件定義
生成された SITEMAP.md を基に、Claude Code とペアプログラミング形式で要件定義書を作成しました:
# 要件定義書 - 浦添囲碁会館 Web サイトリニューアル
## 1. プロジェクト概要
- **目的**: 既存サイトのモダナイゼーション
- **対象**: https://igo.okinawa/ のリプレイス
- **期間**: 3 日間での実装
## 2. 現行サイト分析(SITEMAP.md より)
### ページ構成
- トップページ
- お知らせセクション
- 教室案内
- アクセス情報
- 教室詳細ページ(/lessons)
- 初心者コース
- 中級者コース
- 上級者コース
- 施設案内(/facility)
- 大会情報(/tournaments)
- ブログ(/blog)
- 棋譜(/game-records)
- FAQ(/faq)
## 3. 技術要件
### 必須要件
- SSG(静的サイト生成)による高速化
- レスポンシブデザイン
- SEO 最適化
- 既存コンテンツの完全移行
### 追加要件
- 棋譜プレイヤーの実装
- お問い合わせフォームの改善
- パフォーマンス向上(Lighthouse Score 90 以上)
この要件定義書を基に、Claude Code と対話しながら実装を進めました:
私:「既存サイトのデザインを踏襲しつつ、モダンな技術スタックでリプレイスしたい」
Claude Code:「Next.js 14 の App Router を使用し、SSG で高速化しながら、既存のデザインアセットを活かす構成を提案します」
2. プロジェクト初期設定
Claude Code が提案した構造は、実務でも通用する実践的なものでした:
urasoe_igo/
├── frontend/ # Next.jsアプリケーション
│ ├── src/
│ │ ├── app/ # App Router
│ │ ├── components/ # 再利用可能なコンポーネント
│ │ ├── lib/ # ユーティリティ関数
│ │ └── styles/ # グローバルスタイル
│ ├── public/ # 静的ファイル
│ └── prisma/ # データベーススキーマ
├── backend/ # APIサーバー(必要に応じて)
└── shared/ # 共通型定義
3. コンポーネント開発
私:「トップページにインパクトのあるヒーローセクションを作りたい」
Claude Code の回答と実装:
export const HeroSection: React.FC = () => {
return (
section className="relative h-screen flex items-center justify-center">
div className="absolute inset-0 bg-gradient-to-r from-green-900 to-green-600 opacity-90" />
div className="relative z-10 text-center text-white px-4">
h1 className="text-5xl md:text-7xl font-bold mb-6">浦添囲碁会館h1>
p className="text-xl md:text-2xl mb-8">
伝統と革新が交わる、囲碁の学び舎
p>
button className="bg-white text-green-800 px-8 py-4 rounded-full text-lg font-semibold hover:bg-green-50 transition-colors">
無料体験を申し込む
button>
div>
section>
);
};
4. SSG × ヘッドレス CMS
このプロジェクトでは、Firebase Firestore をヘッドレス CMS として活用し、SSG でコンテンツを静的に生成する構成を採用しました:
import { initializeApp } from "firebase/app";
import { getFirestore, collection, getDocs } from "firebase/firestore";
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
export async function getCMSContent(collectionName: string) {
const snapshot = await getDocs(collection(db, collectionName));
return snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
}
Next.js の SSG 機能を活用
import { Metadata } from "next";
import { getCMSContent } from "@/lib/cms";
export const metadata: Metadata = {
title: "教室案内 | 浦添囲碁会館",
description: "初心者から有段者まで、レベルに応じた囲碁教室をご用意しています",
};
async function getData() {
const lessons = await getCMSContent("lessons");
return lessons;
}
export default async function LessonsPage() {
const lessons = await getData();
return (
div className="container mx-auto px-4 py-16">
h1 className="text-4xl font-bold mb-8">教室案内h1>
div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{lessons.map((lesson) => (
LessonCard key={lesson.id} lesson={lesson} />
))}
div>
div>
);
}
この構成により、以下のメリットを実現:
- 高速な配信: 静的ファイルのため CDN から直接配信
- コンテンツの柔軟な管理: CMS から管理画面でコンテンツ更新可能
- ビルド時の最適化: 必要なデータのみを取得して静的化
- SEO 最適化: 完全にレンダリングされた HTML を配信
- 超低コスト運用: Firebase Hosting の無料枠で実質月額 100 円未満
5. パフォーマンス最適化
Claude Code は画像最適化の重要性も指摘してくれました:
import Image from "next/image";
export const OptimizedImage: React.FC{ src: string; alt: string }> = ({
src,
alt,
}) => {
return (
Image
src={src}
alt={alt}
width={800}
height={600}
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
className="rounded-lg shadow-lg"
/>
);
};
AI 開発ツールの使い分け
1. Claude Code(メイン開発 – 70%)
役割: 大規模な機能実装とアーキテクチャ設計
強み: 200K tokensの長大なコンテキスト理解
例: 囲碁棋譜プレイヤー全体の実装
2. Cursor(軽微な修正 – 15%)
役割: インライン編集と即座の修正
強み: VSCode統合による快適な編集体験
例: タイポ修正、小さなバグフィックス
3. Codex(レビュー対応 – 10%)
役割: レビューコメントに基づく修正実装
強み: レビュー内容を理解した的確な修正
例: レビュアーの指摘事項を自動修正、改善案の実装
4. GitHub Copilot(レビュー支援 – 5%)
役割: コードレビューとPRレビュアー
強み: GitHubとの深い統合
例: PR説明文自動生成、レビューコメント作成、改善提案
効果的な活用法
1. 明確な指示を与える
- 曖昧:「かっこいいデザインにして」
- 具体的:「緑色グラデーション背景、白文字タイトル、レスポンシブ対応」
2. 段階的な実装
- 基本構造 → スタイリング → インタラクション → 最適化
3. コードレビューの活用
- パフォーマンス改善やアクセシビリティの観点でレビュー依頼
遭遇した課題と解決策
課題 1: 状態管理の複雑化
当初、props のバケツリレーが発生していました。
解決策: Context API の導入
const AppContext = createContextAppContextType | undefined>(undefined);
export const AppProvider: React.FC{ children: ReactNode }> = ({
children,
}) => {
const [state, setState] = useState(initialState);
return (
AppContext.Provider value={{ state, setState }}>
{children}
AppContext.Provider>
);
};
課題 2: SEO 対策
静的サイトでも SEO 対策は重要です。
解決策: メタデータの最適化と structured data
export const metadata: Metadata = {
title: "浦添囲碁会館 | 沖縄の囲碁教室",
description: "初心者から有段者まで...",
openGraph: {
title: "浦添囲碁会館",
description: "...",
images: ["/og-image.jpg"],
},
};
const jsonLd = {
"@context": "https://schema.org",
"@type": "EducationalOrganization",
name: "浦添囲碁会館",
};
🎮 囲碁棋譜プレイヤーの実装
プロジェクトで最も技術的に挑戦的だったのは、Web 上で動作する囲碁棋譜プレイヤーの実装でした。
📋 実装例を見る: https://igo.okinawa/records/394/
技術的な要件
- SGF 形式の棋譜データの解析
- 19×19 の碁盤のレンダリング
- 手順の再生・巻き戻し機能
- コメント・変化図の表示
- レスポンシブ対応
実装アプローチ
1. SGF パーサーの実装
SGF(Smart Game Format)は囲碁の棋譜を記録する標準形式です。Claude Code と一緒に、このフォーマットを解析するパーサーを実装しました:
interface SGFNode {
move?: { color: "B" | "W"; position: [number, number] };
comment?: string;
variations?: SGFNode[][];
}
export class SGFParser {
private input: string;
private position: number = 0;
constructor(sgfString: string) {
this.input = sgfString;
}
parse(): SGFNode[] {
return this.parseSequence();
}
private parseSequence(): SGFNode[] {
const nodes: SGFNode[] = [];
while (this.position this.input.length) {
if (this.input[this.position] === ";") {
this.position++;
nodes.push(this.parseNode());
} else if (this.input[this.position] === "(") {
this.position++;
const variation = this.parseSequence();
if (nodes.length > 0) {
nodes[nodes.length - 1].variations =
nodes[nodes.length - 1].variations || [];
nodes[nodes.length - 1].variations.push(variation);
}
} else if (this.input[this.position] === ")") {
this.position++;
break;
} else {
this.position++;
}
}
return nodes;
}
private parseNode(): SGFNode {
const node: SGFNode = {};
while (
this.position this.input.length &&
this.input[this.position] !== ";" &&
this.input[this.position] !== "(" &&
this.input[this.position] !== ")"
) {
const prop = this.parseProperty();
if (prop) {
this.processProperty(node, prop);
}
}
return node;
}
}
2. Canvas API を使用した碁盤のレンダリング
SVG ではなく Canvas API を選択した理由:
- 大量の石を描画する際のパフォーマンス
- アニメーション効果の実装が容易
- ピクセル単位での精密な制御
import { useEffect, useRef, useState } from "react";
interface GoBoardProps {
gameState: GameState;
onCellClick?: (x: number, y: number) => void;
}
export const GoBoard: React.FCGoBoardProps> = ({ gameState, onCellClick }) => {
const canvasRef = useRefHTMLCanvasElement>(null);
const [dimensions, setDimensions] = useState({ width: 600, height: 600 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const updateDimensions = () => {
const container = canvas.parentElement;
if (container) {
const size = Math.min(container.clientWidth, container.clientHeight);
setDimensions({ width: size, height: size });
canvas.width = size;
canvas.height = size;
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
drawBoard(ctx, dimensions);
drawStones(ctx, gameState, dimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, [gameState, dimensions]);
const drawBoard = (ctx: CanvasRenderingContext2D, dimensions: Dimensions) => {
const { width, height } = dimensions;
const cellSize = width / 20;
ctx.fillStyle = "#DCB35C";
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = "#000000";
ctx.lineWidth = 1;
for (let i = 1; i 19; i++) {
ctx.beginPath();
ctx.moveTo(cellSize * i, cellSize);
ctx.lineTo(cellSize * i, cellSize * 19);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cellSize, cellSize * i);
ctx.lineTo(cellSize * 19, cellSize * i);
ctx.stroke();
}
const starPoints = [4, 10, 16];
ctx.fillStyle = "#000000";
starPoints.forEach((x) => {
starPoints.forEach((y) => {
ctx.beginPath();
ctx.arc(cellSize * x, cellSize * y, 3, 0, Math.PI * 2);
ctx.fill();
});
});
};
const drawStones = (
ctx: CanvasRenderingContext2D,
gameState: GameState,
dimensions: Dimensions
) => {
const cellSize = dimensions.width / 20;
gameState.board.forEach((row, y) => {
row.forEach((cell, x) => {
if (cell !== null) {
const centerX = cellSize * (x + 1);
const centerY = cellSize * (y + 1);
const radius = cellSize * 0.45;
ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
if (cell === "black") {
const gradient = ctx.createRadialGradient(
centerX - radius / 3,
centerY - radius / 3,
0,
centerX,
centerY,
radius
);
gradient.addColorStop(0, "#404040");
gradient.addColorStop(1, "#000000");
ctx.fillStyle = gradient;
} else {
const gradient = ctx.createRadialGradient(
centerX - radius / 3,
centerY - radius / 3,
0,
centerX,
centerY,
radius
);
gradient.addColorStop(0, "#FFFFFF");
gradient.addColorStop(1, "#E0E0E0");
ctx.fillStyle = gradient;
}
ctx.fill();
ctx.shadowColor = "transparent";
}
});
});
};
return (
div className="relative w-full aspect-square">
canvas
ref={canvasRef}
className="w-full h-full cursor-pointer"
onClick={handleClick}
/>
div>
);
};
3. 再生コントロールの実装
export const KifuPlayer: React.FC{ sgfData: string }> = ({ sgfData }) => {
const [currentMove, setCurrentMove] = useState(0);
const [gameStates, setGameStates] = useStateGameState[]>([]);
const [isAutoPlaying, setIsAutoPlaying] = useState(false);
useEffect(() => {
const parser = new SGFParser(sgfData);
const moves = parser.parse();
const states = generateGameStates(moves);
setGameStates(states);
}, [sgfData]);
useEffect(() => {
if (!isAutoPlaying) return;
const interval = setInterval(() => {
setCurrentMove((prev) => {
if (prev >= gameStates.length - 1) {
setIsAutoPlaying(false);
return prev;
}
return prev + 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isAutoPlaying, gameStates.length]);
const controls = {
first: () => setCurrentMove(0),
prev: () => setCurrentMove(Math.max(0, currentMove - 1)),
next: () =>
setCurrentMove(Math.min(gameStates.length - 1, currentMove + 1)),
last: () => setCurrentMove(gameStates.length - 1),
toggleAutoPlay: () => setIsAutoPlaying(!isAutoPlaying),
};
return (
div className="space-y-4">
GoBoard gameState={gameStates[currentMove]} />
div className="flex items-center justify-center gap-2">
button onClick={controls.first} className="p-2">
⏮
button>
button onClick={controls.prev} className="p-2">
⏪
button>
button onClick={controls.toggleAutoPlay} className="p-2">
{isAutoPlaying ? "⏸" : "▶️"}
button>
button onClick={controls.next} className="p-2">
⏩
button>
button onClick={controls.last} className="p-2">
⏭
button>
div>
div className="text-center">
手数: {currentMove} / {gameStates.length - 1}
div>
div>
);
};
Claude Code との協働で得た知見
-
複雑なアルゴリズムの段階的実装
- まず基本的な碁盤の描画から始める
- 次に石の配置機能を追加
- 最後に棋譜の解析と再生機能を実装
-
デバッグの効率化
- Claude Code に「この SGF データが正しく解析できない」と具体例を示すと、的確な修正案を提示
-
パフォーマンス最適化
- 「大量の石を描画すると重い」という問題に対し、Canvas API の最適化手法を提案
-
エッジケースの処理
- コウ、セキ、変化図など、囲碁特有のルールへの対応
実装の成果
- 軽量: 外部ライブラリに依存せず、純粋な TypeScript/React で実装
- 高速: Canvas API による効率的なレンダリング
- 柔軟: SGF 形式の様々なバリエーションに対応
- レスポンシブ: モバイルでも快適に閲覧可能
この棋譜プレイヤーの実装は、Claude Code の真価を発揮した例でした。囲碁のルールを理解し、適切なデータ構造を提案し、パフォーマンスを考慮したコードを生成してくれました。
🎮 動作デモ: 実際の棋譜プレイヤーはこちらでご覧いただけます。再生ボタンで自動再生、矢印ボタンで手動操作が可能です。
成果とベストプラクティス
📊 開発成果
- 開発期間: 2-3 週間 → 3 日間
- コード品質: TypeScript 型定義の一貫性、高い再利用性
- パフォーマンス: Lighthouse Score 98 達成
💡 3 つのベストプラクティス
- ペアプログラミング: Claude Code を「もう一人の開発者」として活用
- 学習ツール: 生成コードから新しいパターンを学習
- レビュアー: 見落としを防ぐためのコードレビュー
実践 Tips
const workflow = {
"設計・実装": "Claude Code (70%)",
修正: "Cursor (15%)",
レビュー対応: "Codex (10%)",
レビュー: "Copilot (5%)",
};
効果的なプロンプト例
- Claude Code: 「囲碁棋譜プレイヤーを SGF 形式対応で実装」
- Cursor: 「タイポを修正」
- Codex: 「レビューコメントを反映」
- Copilot: 「潜在的な問題点を指摘」
今すぐ始める 3 ステップ
1. 環境構築(5 分)
npm install -g claude-code-cli
claude-code init my-project
2. 最初のプロンプト
"Next.js 14でSSG対応のWebサイトを作成。
Tailwind CSS使用、TypeScript、
レスポンシブ対応、SEO最適化済み"
3. 3 時間後には公開可能
まとめ
2 週間 →3 日の短縮は本当に可能です。
Claude Code により、以下が実現:
- コーディング時間 88.9%削減
- Lighthouse Score 98 達成(50→98)
- 開発期間 85.7%短縮
- 月額費用 97%削減(Firebase Hosting 活用で 100 円未満)
今すぐアクション
- Claude Codeを今すぐ試す
- この記事のコードをコピペして実行
- あなたの成功事例をコメントで共有
AI 駆動開発は、もはや未来ではなく今ここにある現実です。
📚 参考リンク
著者について
🚀 AI 駆動開発を日々実践中のエンジニア
- 💼 業務:インフラ〜フロントエンドまで AI 駆動で開発
- 🏢 経験:GCP/AWS、オンプレインフラ構築、フルスタック開発 $
開発歴: {new Date().getFullYear() – 2005}年〜 - 🎯 目標:AI 駆動開発のスペシャリストを目指して日々学習中
- ♟️ 趣味:囲碁(浦添囲碁会館で土曜大会参加)
📧 お仕事のご相談
AI 駆動開発のご相談、開発案件のお問い合わせはお気軽にどうぞ!
以下のような案件を承っております:
- 🌐 Web サイト・アプリケーション開発
- 🔄 既存システムの AI 活用リファクタリング
- ☁️ インフラ構築・最適化
- 💡 技術顧問・コンサルティング
連絡先:
- 📧 メール: [email protected]
- 💬 Zenn: DM でお気軽に
- 🐙 GitHub: Issue またはメッセージ
フォローしていただけると嬉しいです!最新の AI 開発テクニックを共有していきます
- 📘 Zenn: この記事の著者をフォロー
- 🐙 GitHub: @sakumoto-shota
Views: 0