• 作成日:

【WordPress】カスタム投稿を使った絞り込み検索をプラグインなしで実装する - ページネーション有り

【WordPress】カスタム投稿を使った絞り込み検索をプラグインなしで実装する - ページネーション有り

今回はWordPressでカスタム投稿とタクソノミー、タームを使った絞り込み検索をプラグインなしで実装する方法を紹介します。

前提条件は以下

  • 1つのカスタム投稿に紐づく複数のタクソノミーとタームで絞り込み
  • search.phpやsearchform.phpは使わない
  • 通常の投稿や固定ページは絞り込みに含めない
  • 絞り込み機能はどのページにも配置できる
  • キーワード検索ではカスタムフィールドのテキストも含めて検索できる
  • キーワード検索は複数ワードに対応
  • ページネーションが使える

説明環境は以下

  • macOS Monterey 12.5.1
  • Visual Studio Code v1.73.1
  • WordPress v6.1.1
この記事の目次

カスタム投稿を使った絞り込み検索を実装する

サンプルページを作ったので以下で確認してみてください。

用意したカスタム投稿とカスタムタクソノミーは以下の通りです。

  • カスタム投稿名 … demo
  • カスタムタクソノミー名① … motion
  • カスタムタクソノミー名② … library
  • カスタムタクソノミー名③ … choice

管理画面上では以下のように表示されています。

必要なファイル構成

WordPressには検索フォームのためのsearchform.phpと、検索結果を表示させるsearch.phpがありますが、今回は使用していません。(通常の検索機能を残すため)

使うファイルは以下です。

  • archive-demo.php … 絞り込み結果を表示させるページ
  • filter-searchform.php … 絞り込み機能

filter-searchform.phpを読み込ませば、どのページでも絞り込み検索が可能です。

カスタム投稿とカスタムタクソノミーを作成する

カスタム投稿とカスタムタクソノミーを作成します。プラグインで作成しても良いですが、今回はfunctions.phpから作成します。

function demo_init() {
  //カスタム投稿【 demo 】
	$labels = array(
		'name'               => _x( 'デモページ', 'post type general name' ),
		'singular_name'      => _x( 'デモページ', 'post type singular name' ),
		'add_new'            => _x( '新規追加', 'demo' ),
		'add_new_item'       => __( '新しくデモページを追加する' ),
		'edit_item'          => __( 'デモページを編集' ),
		'new_item'           => __( '新しいデモページ' ),
		'view_item'          => __( 'デモページを見る' ),
		'search_items'       => __( 'デモページを探す' ),
		'not_found'          => __( 'デモページはありません' ),
		'not_found_in_trash' => __( 'ゴミ箱にデモページはありません' ),
		'parent_item_colon'  => '',
	);
	$args   = array(
		'labels'             => $labels,
		'description'        => 'デモページの一覧ページです。',
		'public'             => true,
		'publicly_queryable' => true,
		'show_ui'            => true,
		'query_var'          => true,
		'rewrite_withfront'  => true,
		'rewrite'            => true,
		'capability_type'    => 'post',
		'hierarchical'       => false,
		'menu_position'      => 5,
		'show_in_rest'       => true, /* カスタム投稿でgutenberg有効化する場合はtrue/false */
		'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
		'has_archive'        => true,

	);
	register_post_type( 'demo', $args );

  //カスタムタクソノミー【 motion 】
	register_taxonomy(
		'motion',
		'demo',
		array(
			'hierarchical'   => true,
			'label'          => 'Motion',
			'labels'         => array(
				'all_items'    => 'Motion一覧',
				'add_new_item' => 'Motionを追加',
			),
			'show_ui'        => true,
			'query_var'      => true,
			'rewrite'        => true,
			'singular_label' => 'Motion',
			'show_in_rest'   => true,
		)
	);
  //カスタムタクソノミー【 library 】
	register_taxonomy(
		'library',
		'demo',
		array(
			'hierarchical'   => true,
			'label'          => 'Library',
			'labels'         => array(
				'all_items'    => 'Library一覧',
				'add_new_item' => 'Libraryを追加',
			),
			'show_ui'        => true,
			'query_var'      => true,
			'rewrite'        => true,
			'singular_label' => 'Library',
			'show_in_rest'   => true,
		)
	);
  //カスタムタクソノミー【 choice 】
	register_taxonomy(
		'choice',
		'demo',
		array(
			'hierarchical'   => false,
			'label'          => 'Choice',
			'labels'         => array(
				'all_items'    => 'Choice一覧',
				'add_new_item' => 'Choiceを追加',
			),
			'show_ui'        => true,
			'query_var'      => true,
			'rewrite'        => true,
			'singular_label' => 'Choice',
			'show_in_rest'   => true,
		)
	);
}
add_action( 'init', 'demo_init' );

あとは投稿画面でタームを作成したときにslugが日本語にならないようにしています。
これはお好みで実装してください。

add_action( 'create_motion', 'post_taxonomy_auto_slug', 10 ); //motion用
add_action( 'create_library', 'post_taxonomy_auto_slug', 10 ); //library用
add_action( 'create_choice', 'post_taxonomy_auto_slug', 10 ); //choice用

function post_taxonomy_auto_slug( $term_id ) {
	$tax  = str_replace( 'create_', '', current_filter() );
	$term = get_term( $term_id, $tax );
	if ( preg_match( '/(%[0-9a-f]{2})+/', $term->slug ) ) {
		$args = array(
			'slug' => $term->taxonomy . '-' . $term->term_id,
		);
		wp_update_term( $term_id, $tax, $args );
	}
}

絞り込み検索機能の実装

今回の絞り込みには以下の4つを用意しました。

  • Motion部分
  • Library部分
  • Choice部分
  • 検索窓

filter-search.phpに以下のように書きます。

<?php
  //どのテンプレートからでも呼び出せるようにグローバル変数を定義
	global $search_paged;
	global $search_args;
	$c_post     = 'demo'; // カスタム投稿【demo】
	$tax_type01 = 'motion'; // タクソノミー【motion】
	$tax_type02 = 'library'; // タクソノミー【library】
	$tax_type03 = 'choice'; // タクソノミー【choice】

if ( is_singular() || is_front_page() ) { //投稿ページや固定ページ、トップページの場合
	$search_paged = get_query_var( 'page' ) ? get_query_var( 'page' ) : 1;
} else { //その他(アーカイブページなど)
	$search_paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1;
}
//キーワード検索欄に入力した単語を取得
$search_word = array();
if ( ! empty( $_GET['f'] ) ) {
	$search_word = $_GET['f'];
	$search_word = str_replace( ' ', ' ', $search_word ); //全角スペースを半角スペースに置き換える(複数単語対応のため)
}
	$search_args = array(
		'post_type'      => $c_post, //カスタム投稿
		'paged'          => $search_paged, // ページ番号を設定
		's'              => $search_word, //検索欄に入力した単語
		'post_status'    => 'publish',
		'posts_per_page' => 10, //10件ずつ表示
		'orderby'        => 'name', //名前順
		'order'          => 'DESC',
	);
  //カスタムタクソノミー【 motion 】部分にあるチェックボックスの内容を取得
	$$tax_type01 = array();
	if ( ! empty( $_GET[ $tax_type01 ] ) ) {
		foreach ( $_GET[ $tax_type01 ] as $value ) {
			$$tax_type01[] = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
		}
		$tax_query_args[] = array(
			'taxonomy'         => $tax_type01,
			'terms'            => $$tax_type01,
			'include_children' => false,
			'field'            => 'slug',
			'operator'         => 'IN',
		);
	}
  //カスタムタクソノミー【 library 】部分にあるチェックボックスの内容を取得
	$$tax_type02 = array();
	if ( ! empty( $_GET[ $tax_type02 ] ) ) {
		foreach ( $_GET[ $tax_type02 ] as $value ) {
			$$tax_type02[] = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
		}
		$tax_query_args[] = array(
			'taxonomy'         => $tax_type02,
			'terms'            => $$tax_type02,
			'include_children' => false,
			'field'            => 'slug',
			'operator'         => 'IN',
		);
	}
  //カスタムタクソノミー【 choice 】部分にあるプルダウン部分の内容を取得
	$$tax_type03 = array();
	if ( ! empty( $_GET[ $tax_type03 ] ) ) {
		foreach ( (array) $_GET[ $tax_type03 ] as $value ) {
			$$tax_type03[] = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );
		}
		$tax_query_args[] = array(
			'taxonomy'         => $tax_type03,
			'terms'            => $$tax_type03,
			'include_children' => false,
			'field'            => 'slug',
			'operator'         => 'IN',
		);
	}
  //カスタムタクソノミーそれぞれの情報をセットする
	if ( ! empty( $_GET[ $tax_type01 ] ) || ! empty( $_GET[ $tax_type02 ] ) || ! empty( $_GET[ $tax_type03 ] ) ) {
		$search_args += array(
			'tax_query' => array(
				'relation' => 'AND',
				array( $tax_query_args ),
			),
		);
	}
	?>
<!-------- 表示させる部分 -------->
<div class="p-filter">
	<div class="p-filter__fixed">
	<form method="get" action="<?php echo esc_url( home_url() ); ?>/<?php echo esc_attr( $c_post ); ?>">
    <!-------- Motionの部分 -------->
    <div class="p-filter__block">
      <div class="p-filter__heading"><span class="main"><?php echo strtoupper( $tax_type01 ); ?></span><span class="sub">- 動きで絞り込む -</span></div>
        <?php
          $terms = get_terms( $tax_type01, array( 'hide_empty' => false ) );
          foreach ( $terms as $term ) :
          $checked = '';
            if ( in_array( $term->slug, $$tax_type01, true ) ) {
            $checked = ' checked';
            }
         ?>
         <label>
           <input type="checkbox" name="<?php echo $tax_type01; ?>[]" class="p-filter__check" value="<?php echo esc_attr( $term->slug ); ?>"<?php echo $checked; ?>>
           <span class="p-filter__check-text"><?php echo esc_html( $term->name ) . '(' . $term->count . ')'; ?></span>
         </label>
         <?php endforeach; ?>
     </div>
     <!-------- Libraryの部分 -------->
     <div class="p-filter__block">
       <div class="p-filter__heading"><span class="main"><?php echo strtoupper( $tax_type02 ); ?></span><span class="sub">- ライブラリで絞り込む -</span></div>
         <?php
           $terms = get_terms( $tax_type02, array( 'hide_empty' => false ) );
           foreach ( $terms as $term ) :
           $checked = '';
             if ( in_array( $term->slug, $$tax_type02, true ) ) {
             $checked = ' checked';
             }
         ?>
         <label>
           <input type="checkbox" name="<?php echo $tax_type02; ?>[]" class="p-filter__check" value="<?php echo esc_attr( $term->slug ); ?>"<?php echo $checked; ?>>
           <span class="p-filter__check-text"><?php echo esc_html( $term->name ) . '(' . $term->count . ')'; ?></span>
         </label>
         <?php endforeach; ?>
      </div>
      <!-------- Choiceの部分 -------->
      <div class="p-filter__block">
        <div class="p-filter__heading"><span class="main"><?php echo strtoupper( $tax_type03 ); ?></span><span class="sub">- 選択肢から絞り込む -</span></div>
          <select class="p-filter__select" name="<?php echo $tax_type03; ?>">
            <option value="">すべて</option>
            <?php
              $terms = get_terms( $tax_type03, array( 'hide_empty' => false ) );
              foreach ( $terms as $term ) :
              $selected = '';
                if ( in_array( $term->slug, $$tax_type03, true ) ) {
                $selected = ' selected';
                }
            ?>
            <option value="<?php echo esc_html( $term->slug ); ?>"<?php echo $selected; ?>><?php echo esc_html( $term->name ); ?></option>
            <?php endforeach; ?>
         </select>
			</div>
			<!-------- 検索欄の部分 -------->
			<div class="p-filter__block">
				<div class="p-filter__heading"><span class="main">KEYWORD</span><span class="sub">- キーワードで絞り込む -</span></div>
				<label>
					<input class="p-filter__input" type="search"
					placeholder="<?php echo esc_attr_x( 'キーワードを入力', 'placeholder' ); ?>"
					value="<?php echo $search_word ? $search_word : ''; ?>" name="f" >
				</label>
			</div>
			<!-------- 絞り込みボタンとクリアボタン -------->
			<div class="p-filter__button">
				<button type="submit" class="p-filter__button-item p-filter__button-item--submit" value="">絞り込み</button>
				<button type="button" class="p-filter__button-item p-filter__button-item--clear" onclick="location.href='<?php echo esc_url( home_url() ); ?>/<?php echo esc_attr( $c_post ); ?>'">クリア</button>
			</div>
		</form>
	</div>
</div>

絞り込み結果を表示させる

今回はカスタム投稿名がdemoなので、絞り込み結果を表示させるテンプレートはarchive-demo.phpになります。(カスタム投稿名がbooksならarchive-books.phpになる)

archive.demo.phpに書くコードは以下です。
表示させる内容はお好みですが、とりあえずサンプルページのコードを載せています。

<?php get_header(); // header.phpを取得! ?>
<!-- main -->
<main class="l-main">
  <div class="p-contents p-contents--demo c-section">
    <div class="p-contents__wrap c-section-inner p-contents__wrap--demo">
      <aside class="p-contents__side p-contents__side--demo">
        <!------ 絞り込み機能を読み込み ------>
        <?php get_template_part( 'filter-searchform' ); ?>
      </aside>
      <div class="p-contents__main">
        <section class="p-dem">
        <!------ global変数をWP_Queryにセット ------>
        <?php
         global $search_paged;
         global $search_args;
         $search_query = new WP_Query( $search_args );
           if ( $search_query->have_posts() ) :
        ?>
         <div class="p-dem__page-number">
           <!------ 表示件数 ------>
           <span class="post">表示件数:全 <?php echo $search_query->found_posts . ' 件'; ?></span>
           <!------ ページ数 ------>
             <?php if ( is_paged() ) : ?>
           <span class="page">- <?php show_page_number( '' ); ?>ページ目</span>
             <?php endif; ?>
         </div>
         <div class="p-dem__main">
           <!------ ループ開始 ------>
           <?php
            while ( $search_query->have_posts() ) :
            $search_query->the_post();
           ?>
           <!-- p-post-card -->
           <article class="p-dem__card">
             <a href="<?php the_permalink(); // 投稿(固定ページ)のリンクを取得! ?>" class="p-dem__link">
               <h2 class="p-dem__head">
               <!------ タイトルを表示 ------>
               <?php if ( get_the_title() ) : ?>
                 <?php
                  $ttl = get_the_title();
                  $ttl = strip_tags( $ttl );
                  $ttl = str_replace( ' ', '', $ttl );
                  $ttl = str_replace( array( "\r\n", "\r", "\n" ), '', $ttl );// 余計な文字列を除去!
                  echo $ttl;
                 ?>
               <?php else : ?>
                 この記事にはタイトルがありません
               <?php endif; ?>
               </h2>
               <div class="p-dem__desc">
               <!------ 抜粋があれば抜粋を表示 ------>
               <?php if ( has_excerpt() ) : ?>
                 <?php echo esc_html( get_the_excerpt() ); ?>
               <!------ 抜粋が無ければコンテンツから100文字を表示 ------>
               <?php else : ?>
                 <?php
                  $p       = get_post( get_the_ID() );
                  $content = strip_shortcodes( $p->post_content );
                  echo wp_html_excerpt( $content, 100, '...' );
                 ?>
               <?php endif; ?>
               </div>
               <div class="p-dem__meta">
               <?php
                // 投稿に紐づくタームの一覧をタクソノミーのラベルと一緒に表示
                $post_object = get_post( $post->ID ); // 投稿オブジェクトを取得
                $post_type   = $post_object->post_type; // 投稿のポストタイプを取得
                $taxonomies  = get_object_taxonomies( $post_type, 'objects' ); // 投稿タイプに関連したすべてのタクソノミーを取得
               ?>
               <?php if ( ! empty( $taxonomies ) ) : ?>
                 <ul class="p-dem__list">
                 <?php foreach ( $taxonomies as $taxonomy_slug => $taxonomy ) : ?>
                   <?php $terms = get_the_terms( $post->ID, $taxonomy_slug ); ?>
                   <?php if ( ! empty( $terms ) ) : ?>
                     <?php foreach ( $terms as $term ) : ?>
                     <li class="p-dem__item p-dem__item--<?php echo esc_attr( $term->taxonomy ); ?>"><?php echo esc_html( $term->name ); ?></li>
                     <?php endforeach; ?>
                   <?php endif; ?>
                 <?php endforeach; ?>
                 </ul>
               <?php endif; ?>
               </div>
             </a>
           </article><!-- /p-post-card -->
           <?php
            endwhile;
            wp_reset_postdata();
           ?>
           <!------ ループ終了 ------>
         </div>
         <!-- ページネーションを表示 -->
         <?php
          if ( function_exists( 'pagination' ) ) :
           pagination( $search_query->max_num_pages, $search_paged );
          endif;
         ?>
       </section>
       <?php else : // 条件分岐:投稿が無い場合は! ?>
       <div class="p-filter__notion">
         <p>投稿が見つかりません。<br>
            記事が存在しないか、すでに削除されている可能性があります。
         </p>
         <a href="<?php echo esc_url( home_url( '/demo' ) ); ?>" class="p-filter__notion-link">絞り込みページに戻る</a>
       </div>
       <?php endif; // 条件分岐終了! ?>
     </div>
   </div>
 </div>
</main><!-- /main -->
<?php
get_footer();

絞り込み機能があるfilter-searchform.phpは8行目で読み込ませています。

<!------ 絞り込み機能を読み込み ------>
<?php get_template_part( 'filter-searchform' ); ?>

filter-searchform.phpにあるグローバル変数を以下のようにarchive-demo.phpで読み込ませて結果を表示させています。

<!------ global変数をWP_Queryにセット ------>
<?php
 global $search_paged;
 global $search_args;
 $search_query = new WP_Query( $search_args );
  if ( $search_query->have_posts() ) :
?>

ページネーションを機能させる

archive-demo.phpにある以下のページネーションを機能させるにはfunctions.phpにコードを加える必要があります。

<!-- ページネーションを表示 -->
<?php
 if ( function_exists( 'pagination' ) ) :
  pagination( $search_query->max_num_pages, $search_paged );
 endif;
?>

functions.phpに以下のコードを加えます。

<?php
function pagination( $max_num_pages = 0, $paged = 1 ) {
	// Don't print empty markup if there's only one page.
	$max_num_pages = $max_num_pages ? (int) $max_num_pages : $GLOBALS['wp_query']->max_num_pages;
	if ( $max_num_pages < 2 ) {
		return;
	}
	$paged        = intval( $paged );
	$pagenum_link = html_entity_decode( get_pagenum_link() );
	$query_args   = array();
	$url_parts    = explode( '?', $pagenum_link );
	if ( isset( $url_parts[1] ) ) {
		wp_parse_str( $url_parts[1], $query_args );
	}
	$pagenum_link = remove_query_arg( array_keys( $query_args ), $pagenum_link );
	$pagenum_link = trailingslashit( $pagenum_link ) . '%_%';
	$format       = $GLOBALS['wp_rewrite']->using_index_permalinks() && ! strpos( $pagenum_link, 'index.php' ) ? 'index.php/' : '';
	$format      .= $GLOBALS['wp_rewrite']->using_permalinks() ? user_trailingslashit( 'page/%#%', 'paged' ) : '?paged=%#%';
	// Set up paginated links.
	$links = paginate_links(
		array(
			'base'      => $pagenum_link,
			'format'    => $format,
			'total'     => $max_num_pages,
			'current'   => $paged,
			'mid_size'  => 2,
			'add_args'  => array_map( 'urlencode_deep', $query_args ),
			'prev_next' => true,
			'prev_text' => __( '<' ),
			'next_text' => __( '>' ),
			'type' => 'list',
		)
	);
	if ( $links ) :
		?>
	<nav class="p-custom-pagenation" role="navigation">
			<?php echo $links; ?>
	</nav><!-- .navigation -->
		<?php
	endif;
}

ページネーションの注意点は管理画面の表示設定で、「1ページに表示する最大投稿数」がfilter-search.phpで指定した表示投稿数と同じ、もしくはそれ以下にする必要があります

サンプルページでは表示設定では10件に設定されています。

そしてfilter-search.phpでは表示件数を同じく10にしています。同じであれば問題ありません。この数字が11や12など表示設定で指定した数字(今回は10件)より大きいとページネーションが正しく機能しなくなります。

$search_args = array(
  'post_type'      => $c_post,
  'paged'          => $search_paged,
  's'              => $search_word,
  'post_status'    => 'publish',
  'posts_per_page' => 10, //表示設定で指定した数字と同じ、もしくは小さくする
  'orderby'        => 'name', 
  'order'          => 'DESC',
);

キーワード検索にカスタムフィールドも含める

キーワード検索の範囲は、通常だとタイトルとコンテンツ、抜粋だけになりますが、今回はカスタムフィールドも含めます。

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

<?php
function custom_search_filter( $search, $wp_query ) {
	global $wpdb;
    //カスタム投稿demoのアーカイブページだけ機能させる
	if ( ! $wp_query->is_post_type_archive( 'demo' ) ) {
			return $search;
	}
	if ( ! isset( $wp_query->query_vars ) ) {
			return $search;
	}
	$search_words = explode( ' ', isset( $wp_query->query_vars['s'] ) ? $wp_query->query_vars['s'] : '' );
	if ( count( $search_words ) > 0 ) {
			$search = '';
      foreach ( $search_words as $word ) {
        if ( ! empty( $word ) ) {
          $search_word = '%' . esc_sql( $word ) . '%';
          $search     .= " AND (
          {$wpdb->posts}.post_title LIKE '{$search_word}'
          OR {$wpdb->posts}.post_content LIKE '{$search_word}'
          OR {$wpdb->posts}.post_excerpt LIKE '{$search_word}'
          OR {$wpdb->posts}.ID IN (
              SELECT distinct post_id
              FROM {$wpdb->postmeta}
              WHERE meta_value LIKE '{$search_word}'
              )
              ) ";
        }
      }
    }
    return $search;
  }
add_filter( 'posts_search', 'custom_search_filter', 10, 2 );

絞り込み結果を表示させるarchive-demo.phpのみ機能させているので、カスタム投稿名を変えている場合は5行目を変更してください。

カスタム投稿名やカスタムタクソノミー名を変更したい場合は?

カスタム投稿名やカスタムタクソノミー名を変更したい場合は、filter-search.phpの5〜8行目を部分をご自身の環境に合わせて変更してください。

以下は例です。

  • カスタム投稿名 … books
  • カスタムタクソノミー① … color
  • カスタムタクソノミー② … category
  • カスタムタクソノミー③ … language
$c_post     = 'books'; // カスタム投稿【books】
$tax_type01 = 'color'; // タクソノミー【color】
$tax_type02 = 'category'; // タクソノミー【category】
$tax_type03 = 'language'; // タクソノミー【language】

あとはカスタム投稿名に合わせてarchive-demo.phpのファイル名をarchive-【カスタム投稿名】.phpに変更します(カスタム投稿がbooksならarchive-books.php)。これで絞り込み検索は機能します。

キーワード検索にカスタムフィールドを含めたい場合はcustom_search_filter関数の以下の部分を書き換えましょう。カスタム投稿がbooksなら以下です。

<?php
function custom_search_filter( $search, $wp_query ) {
	global $wpdb;
    //カスタム投稿booksのアーカイブページだけ機能させる
	if ( ! $wp_query->is_post_type_archive( 'books' ) ) {
			return $search;
	}
// 以下省略…

おわりに

今回はWordPressでカスタム投稿とタクソノミー、タームを使った絞り込み検索をプラグインなしで実装する方法を紹介しました。絞り込み機能がどのページにも設置できて、かつカスタム投稿やカスタムタクソノミーが変更されても、書き直す箇所が少ないので便利です。

サンプルページではチェックボックスとプルダウン形式しかありませんが、ラジオボタン形式でも実装可能です。