WordPress の REST API を利用した表示
概要
- 非同期(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)
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設定
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 変数なので、そのうちアップデートして欲しいです。
ページネーション
/**
* 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'] = '<';
$paging_text['next'] = '>';
$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" >«</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 を渡すのが目的です。
テンプレート
<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