WordPressの全記事一覧をREST APIを使って取得する

WordPress REST APIを使用して投稿データを取得し、ページネーション付きで一覧表示するためのものです。WordPressのサイトURLとREST APIのエンドポイントを設定し、axiosライブラリを使ってデータを非同期で取得します。ユーザーは、ページネーションボタンをクリックすることで、異なるページの投稿を閲覧できます。取得したHTMLコンテンツ(タイトルや抜粋)は、セキュリティのためDOMPurifyを使ってサニタイズ(無害化)されてから表示されます。

環境

vite@7.1.11
react@19.2.0
axios@1.12.2
dompurify@3.3.0
jsdom@27.0.1

リポジトリはこちら

主要な機能とコンポーネントの構造

App.jsx

import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import './App.css'

function App() {
  // WordPressサイトのベースURLを設定
  const API_BASE_URL = 'https://xxxxxx/wp-json/wp/v2';
  const ITEMS_PER_PAGE = 10; // 1ページあたりの表示件数

  const [posts, setPosts] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1); // 全ページ数
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  /**
   * 投稿データを取得する関数
   */
  const fetchPosts = useCallback(async (page) => {

    setIsLoading(true);
    setError(null);

    try {

      const response = await axios.get(`${API_BASE_URL}/posts`, {
        params: {
          page: page,
          per_page: ITEMS_PER_PAGE,
          _embed: true, // アイキャッチ画像などの埋め込み情報を取得
        },
      });

      setPosts(response.data);// 取得した投稿データをセット

      // レスポンスヘッダーから全ページ数と全投稿数を取得
      const totalPagesFromHeader = parseInt(response.headers['x-wp-totalpages'], 10);// 'x-wp-totalpages' ヘッダーには、このクエリに該当する総ページ数が含まれる
      setTotalPages(totalPagesFromHeader);

    } catch (err) {

      console.error("API Fetch Error:", err);
      setError("データの取得中にエラーが発生しました。");

    } finally {

      setIsLoading(false);
    }
  }, []);

  useEffect(() => {// ページ番号が変わるたびにデータを取得

    fetchPosts(currentPage);
  }, [currentPage, fetchPosts]);

  const handlePageChange = (pageNumber) => {

    if (pageNumber >= 1 && pageNumber <= totalPages) {// ページ番号の範囲チェック
      
      setCurrentPage(pageNumber);
      window.scrollTo(0, 0);// スクロールをトップに戻すなど
    }
  };

  if (isLoading) return <p>記事を読み込み中...</p>;
  if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>;
  if (posts.length === 0) return <p>記事が見つかりませんでした。</p>;

  const pageNumbers = Array.from({length: totalPages}, (_, i) => i + 1);// ページ番号ボタンをレンダリングするための配列を生成

  return (
    <div>
      <h1>投稿一覧 (WP REST API)</h1>

      {/* 投稿リストの表示 */}
      {posts.map(post => (
        <div key={post.id} style={{ border: '1px solid #ccc', margin: '10px 0', padding: '15px' }}>
          {/* タイトルはHTMLエスケープされている可能性があるため、dangerouslySetInnerHTMLを使用 */}
          <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          {/* 抜粋も同様 */}
          <p dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
          {/* アイキャッチ画像 (_embedが効いている場合) */}
          {post._embedded && post._embedded['wp:featuredmedia'] && (
            <img 
              src={post._embedded['wp:featuredmedia'][0].source_url} 
              alt={post._embedded['wp:featuredmedia'][0].alt_text}
              style={{ maxWidth: '200px' }}
            />
          )}
          <a href={post.link} target="_blank" rel="noopener noreferrer">詳細へ</a>
        </div>
      ))}

      {/* ページネーションコントロール */}
      <nav style={{ margin: '20px 0' }}>
        <button
          onClick={() => handlePageChange(currentPage - 1)}
          disabled={currentPage === 1}
        >
          « 前へ
        </button>

        {/* ページ番号ボタンのリスト */}
        {pageNumbers.map(number => (
          <button
            key={number}
            onClick={() => handlePageChange(number)}
            style={{
              fontWeight: currentPage === number ? 'bold' : 'normal',
              backgroundColor: currentPage === number ? '#0073aa' : '#f0f0f0',
              color: currentPage === number ? 'white' : 'black',
              border: '1px solid #ccc',
              margin: '0 4px',
              padding: '8px 12px',
              cursor: 'pointer'
            }}
          >
            {number}
          </button>
        ))}

        <button
          onClick={() => handlePageChange(currentPage + 1)}
          disabled={currentPage === totalPages}
        >
          次へ »
        </button>
      </nav>
      <p>現在 {totalPages} ページ中 {currentPage} ページ目を表示中</p>
    </div>
  );
}

export default App;

Appコンポーネントは、WordPressのサイトURLとREST APIのエンドポイントを設定し、axiosライブラリを使ってデータを非同期で取得します 。ユーザーは、ページネーションボタンをクリックすることで、異なるページの投稿を閲覧できます。

状態管理 (useState)

アプリケーションの状態を管理するための主要なステート変数です。

posts
取得した投稿データ(配列)を格納します。

currentPage
現在表示中のページ番号を管理します。

totalPages
APIレスポンスのヘッダーから取得した総ページ数を格納します。

isLoading
データ取得中(ローディング状態)かを示すブール値です。

error
データ取得時にエラーが発生した場合のエラーメッセージを格納します。

定数

API_BASE_URL
WordPress REST APIのベースURL 。

ITEMS_PER_PAGE
1ページあたりの表示件数 。

データの取得 (fetchPosts / useEffect)

fetchPosts 関数
useCallback を使用し、指定されたページ番号の投稿データを非同期で取得します 。axios.get/posts エンドポイントに対し、page(現在のページ番号)、per_page(表示件数)、_embed: true(埋め込み情報)をクエリパラメータとしてリクエストを送信し、成功時にはレスポンスヘッダー x-wp-totalpages から全ページ数を取得して setTotalPages で更新します。

    useEffect フック
    currentPage の値が変更されるたびに fetchPosts が実行され、新しいページのデータが取得されます 。

    ページネーションの操作 (handlePageChange)

    この関数は、指定された pageNumber が 1 以上かつ totalPages 以下の有効な範囲内であるかチェックした上で currentPage ステートを更新し、同時に window.scrollTo(0, 0) で画面をトップへスクロールします。

      UIのレンダリング

      状態に応じた表示

      データの読み込み中、エラー発生時、または記事が 0 件の場合に、それぞれ対応するメッセージを表示します 。

      投稿リストの表示

      posts.map を使用して、取得した投稿のリストを表示します。セキュリティ対策として、各投稿のタイトルと抜粋はインポートしたsanitizeHtml関数でサニタイズ(無害化)され、そのサニタイズされたHTMLはdangerouslySetInnerHTMLでレンダリングされます。アイキャッチ画像は、post._embedded['wp:featuredmedia']の情報がある場合にそのsource_urlを使って表示し、post.linkに基づいた詳細ページへのリンクも提供します。

        ページネーションコントロール

        このページネーションコンポーネントは、総ページ数(totalPages)に基づいてページ番号ボタンを動的に生成し、現在アクティブなページには異なるスタイルを適用します。「前へ」/「次へ」ボタンは handlePageChange 関数でページ番号を増減させますが、現在のページが最初(1)または最後(totalPages)の場合は disabled 属性で無効化されます。また、コンポーネント下部には現在表示中のページと総ページ数が表示されます。

        sanitizeHtml 関数

        この処理では、まずHTMLサニタイズ(無害化)のエンジンとして DOMPurify ライブラリをインポートします。危険性のあるHTML文字列(dirtyHtml)を受け取ると、DOMPurify.sanitize(dirtyHtml) を呼び出します。これにより、<script>タグや onerror 属性といった悪意のあるコードが自動的に除去されます。こうして無害化された安全なHTML文字列(cleanHtml)が App.jsx に返され、投稿リストで dangerouslySetInnerHTML を使って安全に表示されます。