
この記事では、WordPressをバックエンド(記事管理)、Reactをフロントエンド(表示)として組み合わせる「ヘッドレスCMS」構成のブログの作り方を、初心者向けにステップバイステップで解説します。
最終的に、WordPressで書いた記事がReact製のモダンなサイトに表示される状態を目指します。
WordPressとReactとは?
WordPress は、世界のWebサイトの約40%以上で使われているCMS(コンテンツ管理システム)です。記事の作成、画像の管理、プラグインによる機能追加などが簡単にできます。
React は、Meta(旧Facebook)が開発したJavaScriptライブラリです。コンポーネントベースのUI構築が得意で、Facebook、Instagram、Netflixなどの大規模サービスでも採用されています。
なぜ組み合わせるのか? ― ヘッドレスCMSという考え方
通常のWordPressでは、記事の管理も表示もWordPressが担当します。「ヘッドレスCMS」とは、管理(バックエンド)と表示(フロントエンド)を分離するアーキテクチャです。

この構成のメリット:
- 表示速度が速い ― Reactの仮想DOMにより、ページ遷移が高速
- デザインの自由度が高い ― WordPressテーマの制約から解放される
- スマホ対応がしやすい ― レスポンシブデザインを自前で柔軟に構築できる
- セキュリティが向上 ― フロントエンドとバックエンドが分離しているため攻撃面が減る
デメリットも知っておこう:
- 構成が複雑になるため、学習コストが上がる
- WordPressプラグインの一部(テーマ依存のもの)が使えなくなる
- プレビュー機能やSEO対策に追加の工夫が必要
どんな人に向いている? 「WordPressの管理画面は気に入っているけど、サイトの見た目や速度をもっと自由にしたい」という人に最適です。
準備するもの
WordPress側の確認
WordPress 4.7以降であれば、REST APIはデフォルトで有効です。以下のURLにアクセスして、JSON形式のデータが返ってくれば準備OKです。
https://あなたのサイト.com/wp-json/wp/v2/posts
表示されない場合は? 「設定 → パーマリンク」を開き、何も変更せずに「変更を保存」をクリックしてください。これでAPIが有効になります。
ステップ1:Reactプロジェクトを作成する
2024年以降、create-react-app は公式に非推奨となりました。代わりに高速なビルドツール Vite を使います。
npm create vite@latest my-blog -- --template react
cd my-blog
npm install
npm run dev
ブラウザで http://localhost:5173 が開けば成功です。
ステップ2:WordPressから記事を取得する
src/App.jsx を以下のように書き換えます。
import { useState, useEffect } from 'react';
import './App.css';
function App() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ↓ あなたのWordPressサイトのURLに置き換えてください
const WP_API_URL = 'https://あなたのサイト.com/wp-json/wp/v2';
useEffect(() => {
fetch(`${WP_API_URL}/posts?_embed`)
.then(response => {
if (!response.ok) throw new Error('記事の取得に失敗しました');
return response.json();
})
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <p className="loading">読み込み中...</p>;
if (error) return <p className="error">エラー: {error}</p>;
return (
<div className="app">
<header>
<h1>私のブログ</h1>
</header>
<main>
{posts.map(post => (
<article key={post.id} className="post">
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<time>{new Date(post.date).toLocaleDateString('ja-JP')}</time>
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
</article>
))}
</main>
</div>
);
}
export default App;
コードのポイント
?_embedを付けると、アイキャッチ画像やカテゴリ情報も一緒に取得できますdangerouslySetInnerHTMLを使うのは、WordPressが返すHTMLタグ(<p>など)をそのまま表示するためです。XSS対策として信頼できるソース(自分のWordPress)からのみ使いましょう- エラーハンドリングとローディング状態を入れることで、ユーザーに状況を伝えられます
ステップ3:スタイルを整える
src/App.css を以下に置き換えます。
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans JP', sans-serif;
background-color: #f8f9fa;
color: #333;
line-height: 1.8;
}
.app {
max-width: 720px;
margin: 0 auto;
padding: 2rem 1rem;
}
header h1 {
font-size: 1.8rem;
margin-bottom: 2rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #333;
}
.post {
background: #fff;
padding: 1.5rem;
margin-bottom: 1.5rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.post h2 {
font-size: 1.3rem;
color: #1a1a2e;
margin-bottom: 0.5rem;
}
.post time {
font-size: 0.85rem;
color: #888;
}
.post div {
margin-top: 0.8rem;
color: #555;
}
.loading,
.error {
text-align: center;
padding: 2rem;
font-size: 1.1rem;
}
.error {
color: #e74c3c;
}
/* レスポンシブ対応 */
@media (max-width: 768px) {
.app {
padding: 1rem 0.75rem;
}
header h1 {
font-size: 1.4rem;
}
.post {
padding: 1rem;
margin-bottom: 1rem;
}
}
ステップ4:記事の詳細ページを作る
複数ページのルーティングには React Router を使います。
npm install react-router-dom
ファイル構成
src/
├── App.jsx ← ルーティング設定
├── App.css
├── pages/
│ ├── PostList.jsx ← 記事一覧
│ └── PostDetail.jsx ← 記事詳細
└── main.jsx
src/App.jsx(ルーティング設定)
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PostList from './pages/PostList';
import PostDetail from './pages/PostDetail';
import './App.css';
const WP_API_URL = 'https://あなたのサイト.com/wp-json/wp/v2';
function App() {
return (
<BrowserRouter>
<div className="app">
<header>
<h1><a href="/">私のブログ</a></h1>
</header>
<main>
<Routes>
<Route path="/" element={<PostList apiUrl={WP_API_URL} />} />
<Route path="/post/:id" element={<PostDetail apiUrl={WP_API_URL} />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
}
export default App;
src/pages/PostList.jsx(記事一覧)
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
function PostList({ apiUrl }) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`${apiUrl}/posts?_embed`)
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(() => setLoading(false));
}, [apiUrl]);
if (loading) return <p className="loading">読み込み中...</p>;
return (
<div>
{posts.map(post => (
<article key={post.id} className="post">
<Link to={`/post/${post.id}`}>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
</Link>
<time>{new Date(post.date).toLocaleDateString('ja-JP')}</time>
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
</article>
))}
</div>
);
}
export default PostList;
src/pages/PostDetail.jsx(記事詳細)
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
function PostDetail({ apiUrl }) {
const { id } = useParams();
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`${apiUrl}/posts/${id}?_embed`)
.then(res => res.json())
.then(data => {
setPost(data);
setLoading(false);
})
.catch(() => setLoading(false));
}, [apiUrl, id]);
if (loading) return <p className="loading">読み込み中...</p>;
if (!post) return <p className="error">記事が見つかりませんでした</p>;
return (
<article className="post-detail">
<Link to="/" className="back-link">← 記事一覧に戻る</Link>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<time>{new Date(post.date).toLocaleDateString('ja-JP')}</time>
<div
className="post-content"
dangerouslySetInnerHTML={{ __html: post.content.rendered }}
/>
</article>
);
}
export default PostDetail;
ステップ5:検索機能を追加する
PostList.jsx に検索ボックスを追加します。
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
function PostList({ apiUrl }) {
const [posts, setPosts] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`${apiUrl}/posts?_embed&per_page=20`)
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(() => setLoading(false));
}, [apiUrl]);
// クライアント側でフィルタリング(記事数が少ない場合に有効)
const filteredPosts = posts.filter(post =>
post.title.rendered.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) return <p className="loading">読み込み中...</p>;
return (
<div>
<input
type="text"
className="search-box"
placeholder="記事を検索..."
value={searchTerm}
data-blocked-event={(e) => setSearchTerm(e.target.value)}
/>
{filteredPosts.length === 0 ? (
<p className="no-results">該当する記事が見つかりませんでした</p>
) : (
filteredPosts.map(post => (
<article key={post.id} className="post">
<Link to={`/post/${post.id}`}>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
</Link>
<time>{new Date(post.date).toLocaleDateString('ja-JP')}</time>
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
</article>
))
)}
</div>
);
}
export default PostList;
記事が多い場合は? WordPress側のAPIで検索する方が効率的です。
/posts?search=キーワードを使えば、サーバー側で検索してくれます。
よくある問題と解決方法
CORS エラーが出る
異なるドメイン間でAPIを呼び出すと、ブラウザがリクエストをブロックすることがあります(CORSエラー)。
解決方法: WordPressの functions.php に以下を追加します。
add_action('rest_api_init', function() {
remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
add_filter('rest_pre_serve_request', function($value) {
header('Access-Control-Allow-Origin: http://localhost:5173');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
return $value;
});
});
注意: 本番環境では
http://localhost:5173を実際のフロントエンドのURLに変更してください。*(全許可)はセキュリティ上推奨しません。
記事が表示されない
原因はいくつか考えられます。確認順に試してみてください。
- WordPressサイトのURLが正しいか確認する
- ブラウザで
https://あなたのサイト.com/wp-json/wp/v2/postsを直接開いてみる - パーマリンク設定を保存し直す(「設定 → パーマリンク → 変更を保存」)
- セキュリティプラグインがAPIをブロックしていないか確認する
本番環境にデプロイするには?
npm run build
dist フォルダに生成されたファイルを、Vercel、Netlify、またはレンタルサーバーにアップロードします。VercelやNetlifyならGitHubリポジトリと連携して自動デプロイも可能です。
次のステップ
この記事で基本が理解できたら、以下のステップに進んでみましょう。
- Next.js への移行 ― サーバーサイドレンダリング(SSR)でSEO対策を強化。React単体ではSEOが弱いため、本格的なブログにはNext.jsがおすすめです
- WPGraphQL の導入 ― REST APIの代わりにGraphQLを使うと、必要なデータだけを効率的に取得できます
- キャッシュ戦略 ―
SWRやReact Queryを使って、APIリクエストを減らしパフォーマンスを向上させましょう
まとめ
WordPressとReactを組み合わせたヘッドレスCMS構成では、WordPressの使いやすい管理画面はそのままに、Reactでモダンで高速なフロントエンドを構築できます。
この記事で扱った内容は以下の通りです。
- ヘッドレスCMSの概念と、なぜ分離するのか
- Vite + React でプロジェクトを作成
- WordPress REST API から記事を取得・表示
- React Router で一覧ページと詳細ページを実装
- 検索機能の追加
- CORSエラーなどのトラブルシューティング
まずはローカル環境で試してみて、慣れてきたら本番環境へのデプロイやNext.jsへの移行にチャレンジしてみてください。
この記事が役に立ったらフォローしてください

