📄

WordPress の REST API を利用した表示

2022/12/28に公開

概要

  • 非同期(Ajax)で表示更新を行いたい
  • カスタムエンドポイントを設定したい
  • ちょっと入り組んだクエリを投げたい

前提条件

  • WordPress 最新版(6.1.1で確認)
  • カスタム投稿とカスタムタクソノミーで実装(通常投稿でも大丈夫)
  • 「ギャラリー」カスタム投稿と、「ロケ場所」「プラン」カスタムタクソノミーを準備
  • 「ロケ場所」選択と、「プラン」選択で絞り込みたい

実装

投稿設定

function gallery_custom_post_type()
{
    $labels = array(
        'name'          => 'GALLERY',
        'singular_name' => 'GALLERY',
        'menu_name'     => 'GALLERY',
        'edit_item'     => '投稿を編集',
        'view_item'     => '投稿を表示',
    );
    $args = array(
        'labels'        => $labels,
        'public'        => true,
        'has_archive'   => true,
        'menu_position' => 5,
        'rewrite'       => true,
        'supports'      => array('title', 'editor', 'thumbnail', 'revisions', 'author'),
        'taxonomies'    => array('location', 'plan')
    );
    register_post_type('gallery', $args);
    $args = array(
        'label' => 'ロケ場所',
        'public' => false,
        'show_ui' => true,
        'hierarchical' => true
    );
    register_taxonomy('location', 'gallery', $args);
    $args = array(
        'label' => 'プラン',
        'public' => false,
        'show_ui' => true,
        'hierarchical' => true
    );
    register_taxonomy('plan', 'gallery', $args);
}
add_action('init', 'gallery_custom_post_type');

function add_gallery_custom_column_id($column_name, $id)
{
    if ($column_name == 'location') {
        echo get_the_term_list($id, 'location', '', ', ');
    }
    if ($column_name == 'plan') {
        echo get_the_term_list($id, 'plan', '', ', ');
    }
}
add_action('manage_gallery_posts_custom_column', 'add_gallery_custom_column_id', 10, 2);

function gallery_sort_column($columns)
{
    $columns = array(
        'cb' => '<input type="checkbox" />',
        'title' => 'タイトル',
        'author' => '作成者',
        'location' => 'ロケ場所',
        'plan' => 'プラン',
        'date' => '日時',
    );
    return $columns;
}
add_filter('manage_gallery_posts_columns', 'gallery_sort_column');

カスタム投稿とカスタムタクソノミーを作ります。
管理画面一覧ページで紐付いたタクソノミーを表示させると親切です。

JavaScript対応(Ajax)

ajaxTax.js
const galleryLocationList = document.querySelectorAll('.gallery-list-location');
galleryLocationList.forEach(function (target) {
    target.addEventListener("click", fetchGalleryPosts, false);
});
const galleryPlanList = document.querySelectorAll('.gallery-list-plan');
galleryPlanList.forEach(function (target) {
    target.addEventListener("click", fetchGalleryPosts, false);
});

const galleryPagination = document.querySelectorAll('#rest-gallery .post-number a');
galleryPagination.forEach(function (target) {
    target.addEventListener("click", fetchGalleryPosts, false);
});

jQuery(function ($) {
    $(document).ajaxComplete(function () {
        const ajaxgalleryPagination = document.querySelectorAll('#rest-gallery .post-number a');
        ajaxgalleryPagination.forEach(function (target) {
            target.addEventListener("click", fetchGalleryPosts, false);
        });
    });
});

function fetchGalleryPosts(event) {
    jQuery(function ($) {
        //locationの選択状況
        const galleryLocationList = document.querySelectorAll('.gallery-list-location li');
        const convertedGalleryLocationList = [].map.call(galleryLocationList, (element) => {
            return element;
        });

        //locationのタームリストを取得: dataset {term, tax}
        const targetLocationObject = convertedGalleryLocationList.filter(function (element, index) {
            return element.classList.value === 'active';
        });

        let targetLocation = 'none';
        if (Object.keys(targetLocationObject).length) {
            targetLocation = targetLocationObject[0].dataset.term;
        }

        //Planの選択状況
        const galleryPlanList = document.querySelectorAll('.gallery-list-plan li');
        const convertedGalleryPlanList = [].map.call(galleryPlanList, (element) => {
            return element;
        });
        //Planのタームリストを取得: dataset {term, tax}
        const targetPlanObject = convertedGalleryPlanList.filter(function (element, index) {
            return element.classList.value === 'active';
        });
        let targetPlan = 'none';
        if (Object.keys(targetPlanObject).length) {
            targetPlan = targetPlanObject[0].dataset.term;
        }

        const paged = "paged" in event.target.dataset ? event.target.dataset.paged : 1;

        document.getElementById('rest-gallery').innerHTML = `
<div id="loader-bg">
    <div id="loader">
        <div class="loading-circle02"></div>
    </div>
</div>
        `;

        $.ajax({
            type: 'GET',
            url: `${wprest.homeURL}wp-json/v2/gallery-cat/show/${targetLocation}/${targetPlan}/${paged}`,
            timeout: 10000,
            cache: true,
            dataType: 'json',
            beforeSend: function (xhr) {
                xhr.setRequestHeader('X-WP-Nonce', wprest.nonce);
            },
        }).done(function (data, textStatus, jqXHR) {
            // console.log(data)
            document.getElementById('rest-gallery').innerHTML = JSON.parse(data);
        }).fail(function (jqXHR, textStatus, errorThrown) {
            document.getElementById('rest-gallery').innerHTML = `<p class="read-txt">現在、表示する内容はありません</p>`;
            // 失敗時処理
        }).always(function (data_or_jqXHR, textStatus, jqXHR_or_errorThrown) {
            // doneまたはfail実行後の共通処理
        });
    });

}

window.addEventListener('DOMContentLoaded', (event) => {
    jQuery(function ($) {
        $(document).ajaxComplete(function () {
            //Loading
            const loaderBGDom = document.getElementById("loader-bg");
            const loaderDom = document.getElementById("loader");
            if (loaderBGDom) {
                loaderBGDom.classList.add("fadeout-bg");
                loaderDom.classList.add("fadeout-loader");
            }
            //5秒後にまだ残っていたら解除
            setTimeout(function () {
                if (loaderDom === null) return;
                if (loaderDom.classList.contains('fadeout-loader') === false) {
                    loaderBGDom.classList.add("fadeout-bg");
                    loaderDom.classList.add("fadeout-loader");
                }
            }, 5000);
        });
    });
})

ちょっと長いですが、JavaScript側の処理です。
クリック発動のeventListnerをforEachのように設定しています。
これでどのタクソノミーボタンを押しても発火します。
加えてページネーションも必要なのでその分の設定もします。
jQuery記述がところどころ混ざってますが、ajaxCompleteとかはjQueryが楽ですね。

datasetがない場合はnoneとかにして、クエリー判別に使います。

また読み込みに時間かかる場合とかもあるので loading も一応噛ませています。

REST API設定

customRestAPI.php
add_action('rest_api_init', function () {
    register_rest_route('v2/gallery-cat', '/show/(?P<location>(.+))/(?P<plan>(.+))/(?P<paged>[0-9-]+)', [
        'methods' => 'GET',
        'callback' => 'galleryAPIData',
        'args' => [
            'location' => [
                'default' => false,
                'sanitize_callback' => 'sanitize_title',
            ],
            'plan' => [
                'default' => false,
                'sanitize_callback' => 'sanitize_title',
            ],
            'paged' => [
                'default' => 1,
                'validate_callback' => function ($param, $request, $key) {
                    return is_numeric($param);
                }
            ],
        ],
    ]);
});

function galleryAPIData($data)
{
    $args = [
        'posts_per_page'        => 16,
        'post_type'             => 'gallery',
    ];

    $termTitleName  = '';
    if (isset($data['location']) && $data['location'] !== 'none') {
        $termTitleName = urldecode($data['location']);
        $args['tax_query'][] =
            [
                'taxonomy'    => 'location',
                'field'        => 'name',
                'terms'        => urldecode($data['location']),
            ];
    }
    if (isset($data['plan']) && $data['plan'] !== 'none') {
        if (empty($termTitleName)) {
            $termTitleName = urldecode($data['plan']);
        } else {
            $termTitleName .= '/' . urldecode($data['plan']);
        }
        $args['tax_query'][] =
            [
                'taxonomy'    => 'plan',
                'field'        => 'name',
                'terms'        => urldecode($data['plan']),
            ];
    }
    if (empty($termTitleName)) $termTitleName = 'すべての投稿';

    $paged = 1;
    if (isset($data['paged'])) {
        $paged = $data['paged'];
        $args['paged'] = $paged;
    }
    $the_query = new WP_Query($args);
    if ($the_query->have_posts()) :

        $output = '
    <h2 class="u-h2">' . $termTitleName  . '</h2>
    <ul class="list-gallery">';

        while ($the_query->have_posts()) : $the_query->the_post();
            $post_id = $the_query->post->ID;

            $output .= '
            <li>
                <a href="' . get_permalink($post_id) . '">
                    ' . galleryListEyecatch($post_id) . '
                    <h3 class="list-ttl">' . get_the_title($post_id) . '</h3>
                </a>
            </li>
	        ';
        endwhile;
        $output .= '
            </ul>
        ' . gallery_cms_pagination($the_query, $paged);
    else :
        $output = '表示する内容がありません';
    endif;
    wp_reset_postdata();
    return json_encode($output);
}

WP REST APIのカスタムエンドポイント設定とかです。
ポイントは

register_rest_route('v2/gallery-cat', '/show/(?P<location>(.+))/(?P<plan>(.+))/(?P<paged>[0-9-]+)'

このあたりだと思います。
公式: register_rest_route()
コメントで実装例書いてくださってるのが参考になります。
?P は恐らくパラメータで、 <name> がその後受け取るパラメータ名、その後に正規表現だと思います。

'location' => [
        'default' => false,
        'sanitize_callback' => 'sanitize_title',
],

ここでサニタイズをしていますが、これはカスタマイザーあたりでよく使われるやつだったと思います。
以前別案件で、複数の引数を取りたい時にどうすればいいか悩んだのですが、

wp-json/v2/geolocation/show/9420,9496,9964,9967,9969,9984,9987,10639

こういうパラメーターを、

add_action( 'rest_api_init', function(){
	register_rest_route( 'v2/geolocation', '/show/(?P<ids>([\w\-],?)+)', [
		'methods' => 'GET',
		'callback' => 'get_show_data',
		'args' => [
			'term' => [
				'default' => false,
				'sanitize_callback' => 'sanitize_title',
			],
		],
	]);
} );

受け側

$ids = $data['ids'];
$postIDs = explode(",", $ids);

こういう実装で出来ました。

またtax_queryによる絞り込みを対応させています。
tax_queryはデフォルトで「AND検索」なので、記述していませんが、
「OR検索」をしたい場合は 'relation' => 'AND', を含ませる必要があります。

ちゃんと $args に意図したパラメーターが渡ってるか不明、とかの場合は、 return json_encode($args) とかで返してconsole.logで確認するのが良いかと思います。

必要なファイルを読み込み

<?php
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('behavior', get_theme_file_uri() . '/js/behavior.js', array('jquery-core'), null);
    if (is_post_type_archive(['costume-gallery', 'gallery']) || is_tax(['costume-gallery_cat', 'location', 'plan'])) {
        wp_enqueue_script('ajaxTax', get_theme_file_uri() . '/js/ajaxTax.js', array('jquery-core'), null);
        wp_localize_script('ajaxTax', 'wprest', array('homeURL' => home_url('/'), 'nonce' => wp_create_nonce('wp_rest')));
    }
});

enqueue_scriptsでの読み込みです。
js側で利用するサイトURL、wp_nonce を発行しています。
ここで発行されるのがグローバルな var 変数なので、そのうちアップデートして欲しいです。

ページネーション

pagination.php
/**
 * cms_pagination
 * Costume Gallery 投稿一覧のページング
 *
 * @param int 現在のページ数
 * @param int 総ページ数
 * @param int 範囲
 * @return string
 **/
function gallery_cms_pagination(WP_Query $wp_query, int $paged = 1): string
{

  $range = 2;
  // 文言の変更はここから
  $paging_text['prev'] = '&lt;';
  $paging_text['next'] = '&gt;';
  $paging_text['newest'] = '最新';
  $paging_text['oldest'] = '先頭';
  // 文言の変更はここまで

  $content = '';
  $pages = $wp_query->max_num_pages;

  if (1 != $pages) {
    ob_start();

    echo '<nav class="post-number">';

    if ($paged > 1) {
      echo '<a data-paged="' . ($paged - 1) . '">' . $paging_text['prev'] . '</a>';
    }

    if ($paged > 2 && $paged > $range + 1) {
      echo '<a data-paged="1" >&laquo;</a>';
    }

    for ($i = 1; $i <= $pages; $i++) {
      if (1 != $pages && (!($i >= $paged + $range + 1 || $i <= $paged - $range - 1))) {
        echo ($paged == $i) ? '<span class="current">' . $i . '</span>' : '<a data-paged="' . $i . '" class="inactive">' . $i . '</a>';
      }
    }

    if ($paged < $pages) {
      echo '<a data-paged="' . ($paged + 1) . '">' . $paging_text['next'] . '</a>';
    }

    echo '</nav>';

    $content = ob_get_clean();
  }

  return $content;
}

ページネーションです。
通常のページネーションだと get_pagenum_link() あたりを利用しますが、今回は data-paged を渡すのが目的です。

テンプレート

archive.php
<div id="rest-gallery">
    <h2 class="u-h2">ギャラリー一覧</h2>
    <ul class="list-gallery">
        <?php if (have_posts()) : while (have_posts()) : the_post();
        ?>
                <li>
                    <a href="<?php the_permalink(); ?>">
                        <?php echo galleryListEyecatch($post->ID); ?>
                        <h3 class="list-ttl"><?php the_title(); ?></h3>
                    </a>
                </li>

        <?php
            endwhile;
        else :
            echo '<p>現在、表示する内容はありません。</p>';
        endif;
        ?>
    </ul>
    <?php echo gallery_cms_pagination($wp_query); ?>
</div>

最後に表示箇所です。
適当に省略していますが、当然 get_head() get_footer() は必要です。
実際の案件はタクソノミーを選択出来るボタンを並べて、クリック毎に絞り込みが変化する仕様になってます。

Discussion