ClaudeCodeでNext.jsアプリを作る完全ガイド【ブログサイト構築実践】
Next.jsは、Reactベースのフレームワークの中でも特に人気が高く、個人ブログから大規模なECサイトまで幅広く採用されています。一方で「App Routerの使い方が難しい」「サーバーコンポーネントとクライアントコンポーネントの違いが分からない」など、初心者にとってのハードルも年々上がっています。本記事ではClaudeCodeを活用して、シンプルなブログサイトを実際に構築するプロセスを通じて、Next.jsの全体像を掴めるように解説します。プロンプト例とコードサンプルを多数交えていますので、手を動かしながら読み進めてみてください。
結論:ClaudeCodeとNext.jsで作るブログは「速い・安い・運用しやすい」
ClaudeCodeを使ってNext.jsでブログサイトを作ると、以下のメリットがすぐに享受できます。第一に、開発スピードが圧倒的に速いです。プロジェクト作成からデプロイまで、慣れている人なら1〜2時間、初心者でも半日あれば公開できます。第二に、運用コストがほぼゼロです。Cloudflare Workers / Pagesを使えば、個人レベルのトラフィックでは無料枠で十分賄えます。第三に、SEOやパフォーマンスに強い設計が標準で手に入ります。SSG(静的サイト生成)、画像最適化、メタデータ管理など、本来は手作業が大変な部分をフレームワークが面倒見てくれます。
具体的には次のような機能を実装します。Markdownで記事を書き、ファイルベースで管理し、トップページに記事一覧を表示し、個別記事ページを動的に生成し、タグやカテゴリで絞り込めるようにします。デザインはTailwindCSSで整え、最終的にCloudflareにデプロイして公開します。このすべてをClaudeCodeとの対話だけで完成させられるのが、本記事のゴールです。
h2-1. Next.jsプロジェクトの作成と初期設定
まずはClaudeCodeでプロジェクトを立ち上げます。Node.js 20以上が入っていることを確認しておきましょう。
mkdir my-blog && cd my-blog
claude
ClaudeCodeのプロンプトで次のように依頼します。
プロンプト例1:「このフォルダに、Next.js 14(App Router)+ TypeScript + TailwindCSSの構成でブログサイトの土台を作成してください。記事はMarkdownファイル(content/posts配下)で管理する想定です。」
ClaudeCodeは create-next-app を実行し、必要な依存パッケージ(gray-matter、remark、remark-html または next-mdx-remote)をインストールしてくれます。出来上がる初期構成は次のような形です。
my-blog/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── posts/[slug]/page.tsx
├── content/
│ └── posts/
├── lib/
│ └── posts.ts
├── public/
├── next.config.mjs
├── tailwind.config.ts
└── package.json
App Routerではapp/配下のフォルダ構造がそのままURLになります。例えばapp/about/page.tsxを作れば/aboutでアクセスできます。これがPages Router時代と最大の違いです。
h2-2. Markdownで記事を管理する仕組みを作る
ブログ記事の管理方式はいくつかありますが、本記事ではローカルのMarkdownファイルを読み込む方式を採用します。理由は3つあります。1つ目は学習コストが低いこと、2つ目はGitで履歴管理できること、3つ目はサーバーやDBが不要なことです。
content/posts/hello-world.md のような形でファイルを作成し、冒頭にYAML形式のメタデータ(frontmatter)を書きます。
---
title: "はじめての投稿"
date: "2026-05-11"
description: "ブログ初投稿の記事です。"
tags: ["雑記", "はじめに"]
---
これはMarkdownで書いた本文です。**強調**や *斜体*、リスト、コードブロックも使えます。
ClaudeCodeに次のように依頼します。
プロンプト例2:「content/posts配下のMarkdownファイルを全件読み込んで、frontmatterと本文を取り出すユーティリティをlib/posts.tsに作成してください。getAllPosts と getPostBySlug の2つの関数をエクスポートしてください。」
生成されるコードはこのようなイメージです。
// 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 type PostMeta = {
slug: string;
title: string;
date: string;
description?: string;
tags?: string[];
};
export function getAllPosts(): PostMeta[] {
const files = fs.readdirSync(postsDirectory).filter((f) => f.endsWith('.md'));
return files
.map((file) => {
const slug = file.replace(/\.md$/, '');
const raw = fs.readFileSync(path.join(postsDirectory, file), 'utf8');
const { data } = matter(raw);
return { slug, ...(data as Omit<PostMeta, 'slug'>) };
})
.sort((a, b) => (a.date < b.date ? 1 : -1));
}
export function getPostBySlug(slug: string) {
const filePath = path.join(postsDirectory, `${slug}.md`);
const raw = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(raw);
return { slug, meta: data as Omit<PostMeta, 'slug'>, content };
}
このユーティリティが、ブログ全体の心臓部になります。
h2-3. 記事一覧ページを実装する
トップページに記事一覧を表示します。App Routerではapp/page.tsxがトップページに対応します。
// app/page.tsx
import Link from 'next/link';
import { getAllPosts } from '@/lib/posts';
export default function HomePage() {
const posts = getAllPosts();
return (
<main className="max-w-3xl mx-auto px-4 py-10">
<h1 className="text-3xl font-bold mb-8">ブログ記事一覧</h1>
<ul className="space-y-6">
{posts.map((post) => (
<li key={post.slug} className="border-b pb-4">
<Link href={`/posts/${post.slug}`} className="block hover:opacity-70">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-sm text-gray-500">{post.date}</p>
{post.description && <p className="mt-2 text-gray-700">{post.description}</p>}
</Link>
</li>
))}
</ul>
</main>
);
}
ポイントはgetAllPosts()をコンポーネント内で直接呼んでいる点です。これはApp Routerのサーバーコンポーネントだから可能な書き方で、fsモジュールをそのまま使えます。クライアントコンポーネントでは不可能なので、'use client'を付けないように注意しましょう。
h2-4. 個別記事ページを動的に生成する
次に個別記事ページを実装します。動的ルートはapp/posts/[slug]/page.tsxという構造で表現します。
プロンプト例3:「app/posts/[slug]/page.tsxを作成してください。Markdown本文をHTMLに変換して表示し、generateStaticParamsで全記事のslugを返してください。SEOのためにmetadataも適切に出力してください。」
生成されるコードはこちらです。
// app/posts/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from '@/lib/posts';
import { remark } from 'remark';
import html from 'remark-html';
import type { Metadata } from 'next';
export async function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const { meta } = getPostBySlug(params.slug);
return {
title: meta.title,
description: meta.description,
};
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const { meta, content } = getPostBySlug(params.slug);
const processed = await remark().use(html).process(content);
const contentHtml = processed.toString();
return (
<article className="max-w-3xl mx-auto px-4 py-10">
<h1 className="text-3xl font-bold mb-2">{meta.title}</h1>
<p className="text-sm text-gray-500 mb-8">{meta.date}</p>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
</article>
);
}
generateStaticParamsを使うことで、ビルド時に全記事のページが静的HTMLとして書き出されます。これがSSG(Static Site Generation)です。Cloudflareにデプロイすると、各記事はCDNからミリ秒単位で配信されるようになります。
h2-5. デザインをTailwindで整える
proseクラスは@tailwindcss/typographyプラグインが提供する便利なスタイルです。導入は簡単で、ClaudeCodeに依頼すれば一発で設定してくれます。
プロンプト例4:「@tailwindcss/typographyを導入して、Markdown本文にprose-lgが適用されるようにしてください。ダークモードにも対応してください。」
tailwind.config.tsが次のように更新されます。
import type { Config } from 'tailwindcss';
const config: Config = {
content: ['./app/**/*.{ts,tsx}', './content/**/*.md'],
darkMode: 'media',
theme: { extend: {} },
plugins: [require('@tailwindcss/typography')],
};
export default config;
そしてレイアウトでもダークモード対応を入れておくと完璧です。
// app/layout.tsx
import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
{children}
</body>
</html>
);
}
h2-6. タグページとカテゴリ機能を追加する
ブログとしての完成度を上げるためにタグ機能を実装します。app/tags/[tag]/page.tsxを作って、指定タグを含む記事の一覧を表示します。
プロンプト例5:「app/tags/[tag]/page.tsxを作成し、指定したタグを含む記事一覧を表示してください。generateStaticParamsでは全タグを返してください。」
// app/tags/[tag]/page.tsx
import Link from 'next/link';
import { getAllPosts } from '@/lib/posts';
export async function generateStaticParams() {
const tags = new Set<string>();
getAllPosts().forEach((post) => post.tags?.forEach((t) => tags.add(t)));
return Array.from(tags).map((tag) => ({ tag: encodeURIComponent(tag) }));
}
export default function TagPage({ params }: { params: { tag: string } }) {
const tag = decodeURIComponent(params.tag);
const posts = getAllPosts().filter((p) => p.tags?.includes(tag));
return (
<main className="max-w-3xl mx-auto px-4 py-10">
<h1 className="text-2xl font-bold mb-6">タグ: {tag}</h1>
<ul className="space-y-4">
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/posts/${post.slug}`} className="hover:underline">
{post.title}
</Link>
</li>
))}
</ul>
</main>
);
}
これでタグ単位の絞り込み一覧が完成です。記事側のタグ表示にもリンクを張れば回遊性が一気に高まります。
h2-7. 検索機能とRSSフィードの追加
ブログ運営で意外と重宝するのが検索とRSSです。検索はクライアントサイドの簡易実装で十分です。
プロンプト例6:「全記事のメタ情報をJSONとして書き出し、トップページに簡易検索ボックスを追加してください。検索はクライアント側で行い、タイトルとdescriptionの部分一致でフィルタしてください。」
検索コンポーネントは'use client'を使ったクライアントコンポーネントになります。サーバーコンポーネントから渡された記事配列をstateで保持し、入力に応じてフィルタするだけです。
RSSはapp/feed.xml/route.tsを作って動的に生成します。
// app/feed.xml/route.ts
import { getAllPosts } from '@/lib/posts';
export async function GET() {
const posts = getAllPosts();
const items = posts
.map(
(p) => `
<item>
<title><![CDATA[${p.title}]]></title>
<link>https://example.com/posts/${p.slug}</link>
<pubDate>${new Date(p.date).toUTCString()}</pubDate>
<description><![CDATA[${p.description ?? ''}]]></description>
</item>`
)
.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>My Blog</title>
<link>https://example.com</link>
<description>個人ブログのRSS</description>
${items}
</channel>
</rss>`;
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
}
h2-8. Cloudflareへのデプロイと公開設定
完成したサイトはCloudflare Workers / Pagesにデプロイします。Next.jsをCloudflareで動かす際は、@opennextjs/cloudflareを使うのが現在の推奨です。
プロンプト例7:「Next.jsアプリをCloudflare Workersにデプロイできるように@opennextjs/cloudflareを導入してください。wrangler.jsoncを設定し、preview_urlsはfalseにしてください。」
wrangler.jsoncの重要な設定はこのようになります。
{
"name": "my-blog",
"main": ".open-next/worker.js",
"compatibility_date": "2026-05-01",
"preview_urls": false,
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
}
}
preview_urlsをfalseにするのは、Cloudflare Accessでアクセス制御をしている場合に、プレビューURL経由で認証をバイパスされるのを防ぐためです。社内利用のブログでは特に重要なポイントです。
ビルドとデプロイは次の通りです。
npx opennextjs-cloudflare build
npx wrangler deploy
数十秒で本番環境に反映されます。独自ドメインを使いたい場合はCloudflareダッシュボードからカスタムドメインを設定すればOKです。
FAQ
Q1. なぜApp RouterでPages Routerを使わないのですか? A. Next.js 14以降の公式推奨がApp Routerだからです。サーバーコンポーネントによる転送量削減や、レイアウトのネスト構造など、新しい機能はApp Router側にのみ追加されています。
Q2. WordPressから移行する場合のコツは? A. まずエクスポートしたXMLからMarkdownへの変換スクリプトをClaudeCodeに作ってもらいましょう。画像のリンクは相対パスに置換することで管理しやすくなります。
Q3. 画像はどう管理すればよいですか?
A. public/images/配下に置いて、next/imageコンポーネント経由で読み込むのが基本です。Cloudflare Imagesと連携するとさらに高速化できます。
Q4. コメント機能は付けられますか? A. Disqus、Giscus(GitHub Discussions連携)などの外部サービスを埋め込むのが簡単です。スパム対策や運用負荷の面でもおすすめです。
Q5. アクセス解析はどうすればよいですか? A. Cloudflare Web Analyticsは無料・プライバシーフレンドリーで、JavaScriptスニペットを貼るだけで使えます。
Q6. 記事数が増えてもビルドは速いままですか? A. 数百件であれば全く問題ありません。数千件規模になったらISR(Incremental Static Regeneration)の活用やオンデマンドビルドを検討しましょう。
Q7. ブログ以外にどんなサイトが作れますか? A. ドキュメントサイト、ポートフォリオ、ランディングページ、社内ナレッジベースなど、コンテンツ駆動のサイトは何でも作れます。
まとめ
ClaudeCodeとNext.jsの組み合わせは、個人開発の理想形のひとつです。Markdownで気軽に記事を書きながら、世界中に高速配信できるブログが手に入ります。本記事ではプロジェクトの作成から記事管理、SSG、タグ機能、RSS、Cloudflareへのデプロイまでを一気通貫で実践しました。重要なのは、ClaudeCodeに任せきりにせず「なぜこの設計なのか」を問いながら学ぶ姿勢です。そうすれば、ブログ運用を通じてWebアプリ開発全般のスキルが自然と身についていきます。