📘

【WordPress】WP REST APIで独自エンドポイントから自サイトのコンテンツを非同期取得する

に公開

久々にWordPress関連の記事です。
皆さんはWordPressでの非同期処理の方法って聞くと、どんな方法をイメージしますか?
多くの方は【Ajaxでの非同期処理】を真っ先に思い浮かべるかもしれません。

そこで…今回は別の方法として【WP REST APIで自サイトのコンテンツを非同期取得】する方法についてご紹介したいと思います。

WP REST APIの細かな解説は割愛し、実装時に関係する点を中心に書いていきます。

今回のケース

  • 投稿タイプpostに投稿されているデータを取得
  • ログイン中の自分の投稿だけ取得して表示
  • 独自エンドポイントを作成
  • fetchでエンドポイントに接続
  • レスポンシブしやすい数なので12件ずつ取得

こんな感じの具体例で解説していきます。

1:独自エンドポイントを作成する

WordPressには投稿やユーザー情報などを外部から取得できるエンドポイントが最初から使えるように用意されています。

/wp-json/wp/v2/posts
/wp-json/wp/v2/user
など

ただ、不要な既存REST APIはなるべく無効化して独自エンドポイントのみで運用するという設計の方が分かりやすい&レスポンスの内容がカスタマイズしやすい…ということで、今回は独自エンドポイントを作成して対応してみましょう。

追記するコード

functions.phpで以下のコードを追加します。

// APIエンドポイントの登録
add_action( 'rest_api_init', function() {
  register_rest_route( 'wp/v2', '/my-posts', array(
    'methods'  => 'GET',
    'callback' => 'get_my_posts_api',
    'permission_callback' => function() {
      return is_user_logged_in(); // ログイン必須
    }
  ));
});

解説

REST APIの初期化時のアクションフックrest_api_initで独自エンドポイント追加を行います。
実際にエンドポイント登録処理で使うのがregister_rest_routeです。

第一引数にネームスペース名(wp/v2)、第二引数にルート名(/my-posts)を渡します。
第三引数はエンドポイントの設定用のオプションが入った配列ですね。

callbackに、エンドポイントに接続した際に処理されるコールバック関数名(get_my_posts_api)を入れます。(コールバック関数の内容は後述)

permission_callbackが便利で、ここがtrueにならなければコールバック関数が処理されない認証の役割を果たします。
今回は、シンプルにログインしているかを返す関数で認証しています。

2:コールバック関数を書く

次に、登録したエンドポイントに接続した際に処理されるコールバック関数を書いていきます。

追記するコード

// REST APIでの追加取得処理
function get_my_posts_api( WP_REST_Request $request ) {
  if ( !wp_verify_nonce( $_SERVER['HTTP_X_WP_NONCE'], 'wp_rest' ) ) {
    return new WP_Error( 'rest_forbidden', 'nonceの認証に失敗しました', array( 'status' => 401 ) );
  }

  $user_id = get_current_user_id();

  $paged    = max( 1, intval( $request['page'] ) );
  $per_page = intval( $request['per_page'] ) ?: 12;

  $args = array(
    'post_type'      => 'post',
    'post_status'    => 'publish',
    'author'         => $user_id,
    'posts_per_page' => $per_page,
    'paged'          => $paged,
    'orderby'        => array('date' => 'DESC', 'ID' => 'DESC'),
  );

  $query = new WP_Query($args);

  $posts = array();
  while ( $query->have_posts() ) {
    $query->the_post();
    $post_id = get_the_ID();

    $posts[] = array(
      'id'    => $post_id,
      'title' => esc_html( get_the_title() ),
      'link'  => esc_url( get_permalink() ),
      'image' => esc_url( get_the_post_thumbnail_url($post_id, 'medium') )
                  ?: get_theme_file_uri('/assets/img/no-image.jpg'),
    );
  }
  wp_reset_postdata();

  return array(
    'posts'       => $posts,
    'total_pages' => $query->max_num_pages,
    'current'     => $paged,
  );
}

解説

pagedとper_pageの両変数はrequestを通してクエリパラメータから取得・格納します。(パラメータ部分を記述するJS側は後述)
後は結構シンプルなWP_Queryの処理の記述ですね。

posts変数に各投稿のidやタイトル・リンクURL・サムネイルのソースを格納して、returnの連想配列へ。
JS側で使う総ページ数のtotal_pagesや現在ページのcurrentも含めておきます。

3:非同期処理を行うJS読込とwp_localize_scriptを追記

fetchの処理を含むJSの読込と、そのJSにPHPで生成した使える情報を変数として渡すwp_localize_scriptを書いていきます。

追記するコード

wp_enqueue_script( 'profile', get_theme_file_uri( '/assets/js/profile.js' ), array(), date( 'ymdHis', filemtime( get_theme_file_path( '/assets/js/profile.js' ) ) ), array( 'in_footer' => true ) );
// REST APIのログイン認証用
wp_localize_script('profile', 'myPostApi', array(
  'root'  => esc_url_raw(rest_url('wp/v2/')),
  'nonce' => wp_create_nonce('wp_rest')
));

解説

wp_localize_scriptの第一引数には、第三引数で設定した変数の入った配列を渡したいハンドル名を入れます。
前の行で書いている'profile'を指定して、そのJSにエンドポイントのURLをエスケープして使いやすくするrootや認証用のnonceを渡してあげることができます。

esc_url_rawを使ってエスケープすると、クエリで使う&などをちゃんと残してくれます。
rest_urlはREST APIのエンドポイントのURLを取得するので便利ですね。
nonceを使ってREST API用のnonceを渡していますが、このアクション名のwp_restは公式リファレンス内で指定されています。
https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
このリファレンスを日本語訳して下さっている方がおられますので、こちらの方もご紹介↓
https://wp-rest-api.mydocument.jp/using-the-rest-api/authentication/

4:fetch・レンダリング処理するJSを書く

それでは、最後にfetch処理してエンドポイントに接続→レンダリングを行うJavaScriptを書きます。

追記するコード

let page = 1;
const perPage = 12;
let loading = false;
const container = document.querySelector("#my-posts");

async function loadMyPosts() {
  if (loading) return;
  loading = true;

  page++;

  const scrollEnd = document.querySelector("#my-post-scroll-end");
  if ( scrollEnd ) scrollEnd.remove();

  const res = await fetch(`${myPostApi.root}my-spots?page=${page}&per_page=${perPage}`, {
    method: 'GET',
    headers: {
      'X-WP-Nonce': myPostApi.nonce
    },
    credentials: 'same-origin'
  });
  const data = await res.json();

  data.posts.forEach(post => {
    const html = `
      <div class="post-list">
        <a href="${post.link}" class="post-link">
          <img src="${post.image}" alt="${post.title}">
        </a>
        <h3 class="post-title">
          <a href="${post.link}" class="post-link">${post.title}</a>
        </h3>
      </div>
    `;
    container.insertAdjacentHTML("beforeend", html);
  });

  if (data.current < data.total_pages) {
    const scrollButton = '<div id="my-post-scroll-end"></div>';
    container.insertAdjacentHTML("beforeend", scrollButton);
    // まだ次があるならスクロール監視継続
    addScrollObserver();
  }
  loading = false;
}

function addScrollObserver() {
  const scrollEnd = document.querySelector("#my-post-scroll-end");
  if (!scrollEnd) return;

  const observer = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      observer.unobserve(scrollEnd); // 多重呼び出し防止
      loadMyPosts();
    }
  });
  observer.observe(scrollEnd);
}

// 初回はSSR済み → 2ページ目以降を監視開始
addScrollObserver();

解説

初回表示は以下のようにPHP側でレンダリングしている想定です。

<div id="my-posts">
    <div class="post-list">
        <a href="${post.link}" class="post-link">
          <img src="${post.image}" alt="${post.title}">
        </a>
        <h3 class="post-title">
          <a href="${post.link}" class="post-link">${post.title}</a>
        </h3>
    </div><div id="my-post-scroll-end"></div>
</div>

スクロールエンド要素をIntersectionObserverで画面に入ったか確認し、それを一旦削除→コンテナーに取得した投稿を追記→最後のページでなければスクロールエンド要素を生成してコンテナーの末尾に入れる…といった流れです。

無限スクロールなので、ページ増加はインクリメントで1ページずつ増加していく形で対応します。

fetchではwp_localize_scriptで渡されるrootを活用しつつ、pageとperPageもクエリパラメータに含めます。

headersでX-WP-Nonceの値にmyPostApi.nonceとしてwp_localize_scriptのnonceをヘッダーに含めます。

credentials: 'same-origin'で同一オリジンのみにログイン中かどうかが判別できるCookieを送る(他のオリジンからだとCookieは送らない)という安全策を講じています。
same-originに関して、分かりやすく解説して下さっている記事がありました。
大変助かりました↓
https://dev.classmethod.jp/articles/same-site-same-origin/#toc-same-origin-

あとは、取得できた投稿情報のタイトルやリンク、サムネイルのソースなどを当て込んでいってレンダリング内容に含める…という感じの対応です。

このようなステップで、独自エンドポイントを作って自サイトの投稿情報をREST APIで非同期取得することができました。

最後に

WP REST APIと聞くと「外部のサイトからWordPressのコンテンツを取得する手段」という印象が強かったんですが、今回の実装で自サイトのコンテンツの非同期取得でも利用できるという学びが得られました。

今回のケースを応用して、クエリパラメータの調整でカテゴリーなどで絞り込んだ結果も取得できる…といった設計も容易に実現できそうですね。

admin-ajaxを使った非同期処理と比較して、より状況に沿った手段が選択できるようになりたいと思います!

※もしここを調整した方が良いよ!というアドバイスなどがあれば、是非ともお知らせ下さい

以上、WordPressの実装の参考になりましたら幸いです。

Discussion