ブログ一覧に戻る
WordPressとReactで「ヘッドレスCMS」ブログを作ろう
2025/2/14
2026/2/24
42
Web開発Tips

WordPressとReactで「ヘッドレスCMS」ブログを作ろう

シェア

この記事では、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の管理画面は気に入っているけど、サイトの見た目や速度をもっと自由にしたい」という人に最適です。


準備するもの

  1. WordPress サイト ― 既存のサイト、またはローカル環境(Local がおすすめ)
  2. Node.js ― v18以上(公式サイト からインストール)
  3. テキストエディタ ― VS Code を推奨

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に変更してください。*(全許可)はセキュリティ上推奨しません。

記事が表示されない

原因はいくつか考えられます。確認順に試してみてください。

  1. WordPressサイトのURLが正しいか確認する
  2. ブラウザで https://あなたのサイト.com/wp-json/wp/v2/posts を直接開いてみる
  3. パーマリンク設定を保存し直す(「設定 → パーマリンク → 変更を保存」)
  4. セキュリティプラグインがAPIをブロックしていないか確認する

本番環境にデプロイするには?

npm run build

dist フォルダに生成されたファイルを、Vercel、Netlify、またはレンタルサーバーにアップロードします。VercelやNetlifyならGitHubリポジトリと連携して自動デプロイも可能です。


次のステップ

この記事で基本が理解できたら、以下のステップに進んでみましょう。

  • Next.js への移行 ― サーバーサイドレンダリング(SSR)でSEO対策を強化。React単体ではSEOが弱いため、本格的なブログにはNext.jsがおすすめです
  • WPGraphQL の導入 ― REST APIの代わりにGraphQLを使うと、必要なデータだけを効率的に取得できます
  • キャッシュ戦略SWRReact Query を使って、APIリクエストを減らしパフォーマンスを向上させましょう

まとめ

WordPressとReactを組み合わせたヘッドレスCMS構成では、WordPressの使いやすい管理画面はそのままに、Reactでモダンで高速なフロントエンドを構築できます。

この記事で扱った内容は以下の通りです。

  • ヘッドレスCMSの概念と、なぜ分離するのか
  • Vite + React でプロジェクトを作成
  • WordPress REST API から記事を取得・表示
  • React Router で一覧ページと詳細ページを実装
  • 検索機能の追加
  • CORSエラーなどのトラブルシューティング

まずはローカル環境で試してみて、慣れてきたら本番環境へのデプロイやNext.jsへの移行にチャレンジしてみてください。

カテゴリ:Web開発Tipsタグ:ReactWordpress

この記事が役に立ったらフォローしてください