
WordPressの絞り込み検索機能を自作してみた -その3-
わだっつです。
前回までは、検索フォームの自作について、カテゴリー選択のプルダウンとタグ選択のプルダウンを 2カラム(50:50形式) で配置し、検索プラグイン「VK Filter Search」無料版と同等のデザインにしました。
本記事は先月、「SWELL」で立ち上げたサイトで実装しています。
過去記事は↓こちらから
今回は、ChatGPTを使ってここから更なるカスタマイズを行いました。
実装の目的
本実装の目的は、ユーザーが入力したキーワードに応じて関連するカテゴリーとタグを取得し、検索を補助することです。
具体的には、以下のような動作を実現します。
入力したキーワードが含まれるタイトルに設定されたカテゴリーおよびタグ「のみ」を プルダウンメニューに表示し、それ以外は表示させないようにした
各種エラーメッセージの表示を簡略化し、見出し「投稿記事を検索」の直下にまとめて表示させるようにした。
検索対象を投稿ページのみに
アルファベットの大文字・小文字および全角・半角の区別をなくす
「ケ」の字の大小の区別をなくす(市ケ谷・市ヶ谷、霞ケ関、霞ヶ関など)
キーワード入力がない場合、エラーメッセージを表示し、検索ボタンを無効化
検索結果が0件だった場合もエラーメッセージを表示し、検索ボタンを無効化
キーワードに一致するカテゴリーやタグがあれば、プルダウンを有効化
HTMLの全体構造
まずは、HTMLの全体構造から。
以下がHTMLのソースコードです。※それぞれの処理については後述で詳しく説明します。
<div id="error-message"></div> <!--エラーメッセージ表示用 -->
<form method="get" action="<?php bloginfo('url'); ?>" onsubmit="return validateSearch()">
<div class="search-box-2column">
<!-- キーワード検索 -->
<div class="input-group-2column">
<input name="s" id="search-keyword" type="text" value="<?php the_search_query(); ?>" onkeyup="fetchRelatedCategoriesAndTags()" />
</div>
<!-- カテゴリーとタグのプルダウン(2カラムレイアウト) -->
<div class="search-select-wrapper">
<div class="input-group-2column select-half">
<select name="category" id="category-select" class="custom-select" disabled>
<option value="">カテゴリーを選択</option>
</select>
</div>
<div class="input-group-2column select-half">
<select name="tag" id="tag-select" class="custom-select" disabled>
<option value="">タグ選択</option>
</select>
</div>
</div>
<!-- 検索ボタン -->
<button class="search-button" id="search-button" disabled><i class="fas fa-search"></i> 検索</button>
</div>
</form>
処理の説明①:キーワード入力時の処理
キーワードが入力されるたびに「fetchRelatedCategoriesAndTags()」 を実行
Ajaxを使ってサーバーにリクエストを送信し、関連カテゴリー・タグを取得
取得したデータをプルダウンメニューに追加
選択肢がなければエラーメッセージを表示し、検索を無効化
※「fetchRelatedCategoriesAndTags()」の処理内容については、後述のJavaScriptを参照。
処理の説明②:エラーハンドリング
<div id="error-message"></div> <!-- エラーメッセージ表示用 -->
キーワード未入力時:「キーワードが未入力です」と表示
検索結果なし:「キーワードに一致した記事は見つかりませんでした」と表示
処理の説明③:ユーザー体験の向上
入力が有効(検索結果が1以上ある場合)ならカテゴリー選択・タグ選択のプルダウンと検索ボタンの押下を有効化し、検索しやすくする。⇒ 標準の機能である「検索結果が0件となり、前のページに戻る操作」が不要となる
この処理により、ユーザーは不要な選択肢を避け、効率的に検索できるようになります。
JavaScriptの全体構造
続いてJavaScriptの全体構造から。
以下がJavaScriptのソースコードです。※それぞれの処理については後述で詳しく説明します。
document.addEventListener("DOMContentLoaded", function() {
// 各フォーム要素を取得
const keywordInput = document.getElementById('search-keyword'); // キーワード入力欄
const categorySelect = document.getElementById('category-select'); // カテゴリープルダウン
const tagSelect = document.getElementById('tag-select'); // タグプルダウン
const searchButton = document.getElementById('search-button'); // 検索ボタン
const errorMessageDiv = document.getElementById('error-message'); // エラーメッセージ表示用の要素
let isKeywordEmpty = true; // キーワードが空かどうかを判定するフラグ
let isNoResults = false; // 検索結果がゼロかどうかを判定するフラグ
/**
* キーワードに基づいて関連するカテゴリーとタグを取得する関数
*/
function fetchRelatedCategoriesAndTags() {
let keyword = keywordInput.value.trim(); // 入力値の前後の空白を除去
console.log("🔍 キーワード入力:", keyword);
// キーワードが入力されているかを確認
if (keyword.length > 0) {
isKeywordEmpty = false;
errorMessageDiv.innerHTML = ""; // エラーメッセージをクリア
let xhr = new XMLHttpRequest();
xhr.open('POST', '/wp-admin/admin-ajax.php', true); // WordPressのAjaxを使用
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log("Ajaxレスポンス:", xhr.responseText);
if (xhr.status === 200) {
let response = JSON.parse(xhr.responseText);
if (!response.success) {
resetForm("error");
return;
}
// 検索結果の有無を判定
const hasCategories = response.data.categories && response.data.categories.length > 0;
const hasTags = response.data.tags && response.data.tags.length > 0;
// カテゴリーとタグが両方空ならフォームをリセット
if (!hasCategories && !hasTags) {
isNoResults = true;
resetForm("no-results");
return;
}
isNoResults = false;
errorMessageDiv.innerHTML = ""; // エラーメッセージを削除
// カテゴリーのプルダウンを更新
categorySelect.innerHTML = '<option value="">カテゴリーを選択</option>';
if (hasCategories) {
response.data.categories.forEach(cat => {
categorySelect.innerHTML += `<option value="${cat.term_id}">${cat.name}</option>`;
});
}
// タグのプルダウンを更新
tagSelect.innerHTML = '<option value="">タグ選択</option>';
if (hasTags) {
response.data.tags.forEach(tag => {
tagSelect.innerHTML += `<option value="${tag.slug}">${tag.name}</option>`;
});
}
// 検索結果があった場合のみフォームを有効化
enableForm();
}
}
};
// Ajaxリクエストを送信
xhr.send(`action=get_filtered_terms&keyword=${encodeURIComponent(keyword)}`);
} else {
isKeywordEmpty = true;
resetForm("empty");
}
}
/**
* フォームをリセットし、エラーメッセージを表示する関数
* @param {string} reason - リセットの理由(empty, no-results, error)
*/
function resetForm(reason) {
// プルダウンメニューを初期化
categorySelect.innerHTML = '<option value="">カテゴリーを選択</option>';
tagSelect.innerHTML = '<option value="">タグ選択</option>';
// プルダウンメニューと検索ボタンを無効化
categorySelect.setAttribute("disabled", "disabled");
tagSelect.setAttribute("disabled", "disabled");
searchButton.setAttribute("disabled", "disabled");
// エラーメッセージを表示
if (reason === "empty") {
errorMessageDiv.innerHTML = '<span style="color: red; font-weight: bold;">キーワードが未入力です</span>';
} else if (reason === "no-results") {
errorMessageDiv.innerHTML = '<span style="color: red; font-weight: bold;">キーワードに一致した記事は見つかりませんでした</span>';
}
}
/**
* フォームを有効化する関数
*/
function enableForm() {
categorySelect.removeAttribute("disabled"); // カテゴリー選択を有効化
tagSelect.removeAttribute("disabled"); // タグ選択を有効化
searchButton.removeAttribute("disabled"); // 検索ボタンを有効化
errorMessageDiv.innerHTML = ""; // エラーメッセージを消す
}
// キーワード入力時にAjax検索を実行
keywordInput.addEventListener("input", fetchRelatedCategoriesAndTags);
});
各処理の詳細①:フォーム要素の取得
最初に、HTMLの各フォーム要素のIDを取得します。
const keywordInput = document.getElementById('search-keyword');
const categorySelect = document.getElementById('category-select');
const tagSelect = document.getElementById('tag-select');
const searchButton = document.getElementById('search-button');
const errorMessageDiv = document.getElementById('error-message');
キーワード入力欄(input)
カテゴリー選択・タグ選択のプルダウン(select)
検索ボタン(button)
エラーメッセージ表示用(div)
各処理の詳細②:キーワード入力時にAjaxリクエストを送信
ユーザーがキーワードを入力すると、その値を 「fetchRelatedCategoriesAndTags()」 で取得し、WordPressのAjax API にリクエストを送信します。
let xhr = new XMLHttpRequest();
xhr.open('POST', '/wp-admin/admin-ajax.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(`action=get_filtered_terms&keyword=${encodeURIComponent(keyword)}`);
これにより、サーバーから関連するカテゴリーとタグのデータを取得します。
各処理の詳細③:キーワード入力時にAjaxリクエストを送信
サーバーからのレスポンスを解析し、該当するカテゴリー・タグがあるかをチェックします。
const hasCategories = response.data.categories && response.data.categories.length > 0;
const hasTags = response.data.tags && response.data.tags.length > 0;
if (!hasCategories && !hasTags) {
isNoResults = true;
resetForm("no-results");
return;
}
検索結果がない場合は、「resetForm("no-results")」 を実行し、見出しの直下にエラーメッセージを表示させます。
各処理の詳細④:フォームのリセット
検索結果がない、またはキーワードが未入力の場合、以下の「resetForm()」 を実行し、フォームを無効化 します。
function resetForm(reason) {
categorySelect.innerHTML = '<option value="">カテゴリーを選択</option>';
tagSelect.innerHTML = '<option value="">タグ選択</option>';
categorySelect.setAttribute("disabled", "disabled");
tagSelect.setAttribute("disabled", "disabled");
searchButton.setAttribute("disabled", "disabled");
if (reason === "empty") {
errorMessageDiv.innerHTML = '<span style="color: red; font-weight: bold;">キーワードが未入力です</span>';
} else if (reason === "no-results") {
errorMessageDiv.innerHTML = '<span style="color: red; font-weight: bold;">キーワードに一致した記事は見つかりませんでした</span>';
}
}
以上がJavaScriptのソースコードと処理の詳細です。
CSSの全体構造
CSSでは、検索フォームのデザインを整え、2カラムレイアウトを実現するためのスタイル設定を行っています。
また、スマホ(幅768px以下)では自動的に1カラム表示へ変更し、レスポンシブ対応をしています。
以下がCSSのソースコードです。
/* 検索ボックスとカテゴリー・タブ一覧プルダウン */
.input-group-2column input,
.input-group-2column select {
width: 100%;
border: 1px solid #ccc;
font-size: 16px;
outline: none;
margin-bottom: 15px; /* プルダウン同士の間隔 */
}
/* 検索ボタン(ボタン色はサイトカラー) */
.search-button {
margin-top: 20px; /* プルダウンとの間隔を広げる */
background-color: #007461;
color: white;
border: none;
padding: 12px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s ease;
}
/* 検索ボタン(カーソルを合わせた際のボタン色を指定)*/
.search-button:hover {
background-color: #00aa94;
}
/* 2カラムレイアウト */
.search-select-wrapper {
display: flex;
gap: 20px; /* 隙間を追加 */
}
/* 各セレクトボックスを50%に */
.select-half {
width: 50%;
}
/* カテゴリーとタグのプルダウンを統一 */
.custom-select {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
}
/* 2カラムレイアウト */
.search-select-wrapper {
display: flex;
gap: 10px;
}
.select-half {
flex: 1;
}
/* 無効化された要素のスタイル */
select:disabled, .search-button:disabled {
background-color: #ddd;
cursor: not-allowed;
}
/* スマホ(幅768px以下)で1列に変更 */
@media screen and (max-width: 768px) {
.search-select-wrapper {
flex-direction: column; /* 縦並び */
gap: 20px; /* スマホ時の間隔 */
}
.select-half {
width: 100%; /* フル幅で表示 */
}
}
以下がCSSの各処理の説明です。
検索ボックスとプルダウンのスタイル
.input-group-2column input,
.input-group-2column select {
width: 100%;
border: 1px solid #ccc;
font-size: 16px;
outline: none;
margin-bottom: 15px; /* プルダウン同士の間隔 */
処理内容は以下の通り。
検索ボックスとプルダウン(<input> と <select>)の共通スタイルを適用
width: 100% → フォームの幅を100%に設定
border: 1px solid #ccc → 薄いグレーの枠線を適用
font-size: 16px → フォントサイズの設定
outline: none → クリック時の青い枠を消す
margin-bottom: 15px → プルダウン同士の間隔を調整
検索ボタンのスタイル
.search-button {
margin-top: 20px; /* プルダウンとの間隔を広げる */
background-color: #007461;
color: white;
border: none;
padding: 12px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background 0.3s ease;
}
処理内容は以下の通り。
margin-top: 20px → カテゴリー選択・タグ選択のプルダウンとの間隔を調整
background-color: #007461 → ボタンの背景色をサイトカラーに。こちらはサイトカラーに合わせて任意で指定します。
color: white → 文字色を白に
border: none → 枠線をなくす
padding: 12px → 余白を適用してボタンを見やすく
cursor: pointer → マウスポインターを「手の形」に変更
font-size: 16px → フォントサイズを統一
font-weight: bold → ボタンの文字を太字に
transition: background 0.3s ease → 色がスムーズに変わるようにアニメーションを適用
検索ボタンのホバー(カーソルを合わせた時)
.search-button:hover {
background-color: #00aa94;
}
処理内容は以下の通り。
background-color: #00aa94 → カーソルを合わせると、少し明るい緑色に変わる
ホバー時の視認性を向上し、ユーザーにボタンの反応を感じさせる
2カラムレイアウトの適用
.search-select-wrapper {
display: flex;
gap: 20px; /* 隙間を追加 */
}
処理内容は以下の通り。
display: flex → カテゴリー・タグのプルダウンを横並びに配置
gap: 20px → 2つのプルダウンの間隔を20pxに設定
各セレクトボックスの幅を50%に設定
.select-half {
width: 50%;
}
処理内容は以下の通り。
width: 50% → カテゴリーとタグのプルダウンの幅をそれぞれ50%に設定
プルダウンが均等に配置されるように調整
プルダウンのデザイン統一
.custom-select {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
}
処理内容は以下の通り。
width: 100% → プルダウンの幅を親要素いっぱいに広げる
padding: 10px → 適度な余白を設定し、入力しやすくする
border: 1px solid #ccc → 薄いグレーの枠線を適用
border-radius: 5px → 角を丸くして柔らかい印象に
background-color: #fff → 背景を白に設定し、視認性を向上
2カラムレイアウトの適用(重複)
.search-select-wrapper {
display: flex;
gap: 10px;
}
.select-half {
flex: 1;
}
.search-select-wrapper { display: flex; } → カテゴリーとタグを横並びに配置
.select-half { flex: 1; } → プルダウンを均等に広げる
無効化された要素のデザイン
select:disabled, .search-button:disabled {
background-color: #ddd;
cursor: not-allowed;
}
処理内容は以下の通り。
background-color: #ddd → 無効化された要素(プルダウン・ボタン)の背景色を灰色に変更
cursor: not-allowed → カーソルを「禁止マーク」にし、選択できないことを視覚的に示す
キーワードが入力されるまでカテゴリー選択・タグ選択のプルダウンメニューの選択および検索ボタンが押せないようにする
スマホ(幅768px以下)で1カラムに変更
@media screen and (max-width: 768px) {
.search-select-wrapper {
flex-direction: column; /* 縦並び */
gap: 20px; /* スマホ時の間隔 */
}
.select-half {
width: 100%; /* フル幅で表示 */
}
}
処理内容は以下の通り。
スマホ表示時(幅768px以下)に適用されるスタイル
.search-select-wrapper { flex-direction: column; } → プルダウンを縦並びにする
.select-half { width: 100%; } → プルダウンの幅を100%にし、画面幅いっぱいに広げる
gap: 20px → プルダウン同士の間隔を20pxに広げる
実装ポイント
このCSSでは、検索フォームの見た目を整え、2カラムレイアウトを適用しつつ、スマホでは1カラム表示になるようにレスポンシブ対応を行っています。
カテゴリー・タグのプルダウンを2カラム(50:50)で配置
検索ボタンのデザインを統一し、サイトのブランドカラーを適用
ホバー時に色が変わるアニメーションを追加
検索結果がない場合や未入力時にボタンを無効化し、灰色表示
スマホ時(768px以下)では自動的に縦並びに変更し、操作しやすく
この実装により、PCでもスマホでも使いやすい検索フォームが完成します。
また、プルダウンのデザイン統一や、未入力時の無効化など、ユーザーの操作ミスを防ぐ工夫も取り入れています。
functions.phpの全体構造
この functions.php では、主に以下の機能を追加しています。
子テーマの style.css を適切に読み込む処理
検索フォームのショートコードを作成し、ページに簡単に挿入できるようにする処理
検索キーワードに基づいて関連するカテゴリー・タグを取得する Ajax 検索処理
Ajax リクエストを受け付ける処理
functions.phpのソースコードは以下の通り
<?php
/* 子テーマのfunctions.phpは、親テーマのfunctions.phpより先に読み込まれることに注意してください。 */
/**
* 親テーマのfunctions.phpのあとで読み込みたいコードはこの中に。
*/
// add_filter('after_setup_theme', function(){
// }, 11);
/**
* 子テーマでのファイルの読み込み
*/
add_action('wp_enqueue_scripts', function() {
$timestamp = date( 'Ymdgis', filemtime( get_stylesheet_directory() . '/style.css' ) );
wp_enqueue_style( 'child_style', get_stylesheet_directory_uri() .'/style.css', [], $timestamp );
/* その他の読み込みファイルはこの下に記述 */
}, 11);
// 絞り込み検索(refine_searchform)のショートコード
function shortcode_refine_searchform () {
ob_start();
get_template_part('refine_searchform');
return ob_get_clean();
}
add_shortcode('refine_searchform', 'shortcode_refine_searchform');
// 絞り込み検索(2カラム:refine_searchform_2column)のショートコード
function shortcode_refine_searchform_2column () {
ob_start();
get_template_part('refine_searchform_2column');
return ob_get_clean();
}
add_shortcode('refine_searchform_2column', 'shortcode_refine_searchform_2column');
// 検索時に全角・半角、ひらがな・カタカナを区別しないようにする
function change_search_char($where, $obj) {
if ($obj->is_search) {
$where = str_replace(".post_title", ".post_title COLLATE utf8_unicode_ci", $where);
$where = str_replace(".post_content", ".post_content COLLATE utf8_unicode_ci", $where);
}
return $where;
}
add_filter('posts_where', 'change_search_char', 10, 2);
// 検索キーワードの正規化
function normalize_search_keyword($keyword) {
// 半角・全角、ひらがな・カタカナを統一
$normalized_keyword = mb_convert_kana($keyword, 'asKVC');
// 「ケ」と「ヶ」の違いを統一
$normalized_keyword = str_replace(['ヶ', 'ケ'], 'ヶ', $normalized_keyword);
// 小文字統一
return mb_strtolower($normalized_keyword);
}
// Ajax検索で使用する関数
function get_filtered_terms() {
if (!isset($_POST['keyword']) || empty($_POST['keyword'])) {
wp_send_json_success(array('categories' => [], 'tags' => []));
}
global $wpdb;
$keyword = sanitize_text_field($_POST['keyword']); // キーワードのサニタイズ
// 検索キーワードを正規化
$normalized_keyword = normalize_search_keyword($keyword);
// 投稿のタイトルのみを対象に検索(固定ページを除外)
$post_ids = $wpdb->get_col($wpdb->prepare("
SELECT ID FROM $wpdb->posts
WHERE post_type = 'post'
AND post_status = 'publish'
AND post_title COLLATE utf8_unicode_ci LIKE %s
", '%' . $wpdb->esc_like($normalized_keyword) . '%'));
if (empty($post_ids)) {
wp_send_json_success(array('categories' => [], 'tags' => []));
}
// 関連するカテゴリーを取得
$categories = get_terms(array(
'taxonomy' => 'category',
'hide_empty' => false,
'object_ids' => $post_ids
));
// 関連するタグを取得
$tags = get_terms(array(
'taxonomy' => 'post_tag',
'hide_empty' => false,
'object_ids' => $post_ids
));
// JSONレスポンスを返す
wp_send_json_success(array(
'categories' => $categories,
'tags' => $tags
));
}
// Ajaxリクエストの受付(ログインユーザー & 非ログインユーザー両方対応)
add_action('wp_ajax_get_filtered_terms', 'get_filtered_terms');
add_action('wp_ajax_nopriv_get_filtered_terms', 'get_filtered_terms');
各処理の詳細は以下の通り。
子テーマの style.css を適切に読み込む
/**
* 子テーマでのファイルの読み込み
*/
add_action('wp_enqueue_scripts', function() {
$timestamp = date( 'Ymdgis', filemtime( get_stylesheet_directory() . '/style.css' ) );
wp_enqueue_style( 'child_style', get_stylesheet_directory_uri() .'/style.css', [], $timestamp );
/* その他の読み込みファイルはこの下に記述 */
}, 11);
filemtime( get_stylesheet_directory() . '/style.css' )
→ style.css の最終更新時刻を取得date( 'Ymdgis', filemtime(...) )
→ YYYYMMDDhhmmss の形式に変換し、バージョン番号として使用wp_enqueue_style()
→ style.css を子テーマから読み込む(キャッシュが残らないようにするため), 11 → 親テーマのCSSより後に適用されるように、優先度を 11 に設定
ショートコードで検索フォームを簡単に挿入できるようにする
// 絞り込み検索(2カラム:refine_searchform_2column)のショートコード
function shortcode_refine_searchform_2column () {
ob_start();
get_template_part('refine_searchform_2column');
return ob_get_clean();
}
add_shortcode('refine_searchform_2column', 'shortcode_refine_searchform_2column');
get_template_part('refine_searchform_2column');
→ refine_searchform_2column.php のテンプレートを読み込むob_start(); ... return ob_get_clean();
→ HTMLの出力をバッファリングして、ショートコードとして機能させるadd_shortcode('refine_searchform_2column', 'shortcode_refine_searchform_2column');
→ [refine_searchform_2column] で2カラム版の検索フォームを呼び出せるようにする
検索時(検索ボタン押下時)に全角・半角、ひらがな・カタカナを区別しない
function change_search_char($where, $obj) {
if ($obj->is_search) {
$where = str_replace(".post_title", ".post_title COLLATE utf8_unicode_ci", $where);
$where = str_replace(".post_content", ".post_content COLLATE utf8_unicode_ci", $where);
}
return $where;
}
add_filter('posts_where', 'change_search_char', 10, 2);
処理内容は以下の通り。
posts_where フィルターを使い、検索クエリの WHERE 条件をカスタマイズ
utf8_unicode_ci を適用して、半角・全角、ひらがな・カタカナ、大文字・小文字を区別しないようにする
is_search チェックにより、検索時のみ適用 される
検索キーワードの正規化
function normalize_search_keyword($keyword) {
// 半角・全角、ひらがな・カタカナを統一
$normalized_keyword = mb_convert_kana($keyword, 'asKVC');
// 「ケ」と「ヶ」の違いを統一
$normalized_keyword = str_replace(['ヶ', 'ケ'], 'ヶ', $normalized_keyword);
// 小文字統一
return mb_strtolower($normalized_keyword);
}
処理内容は以下の通り。
mb_convert_kana($keyword, 'asKVC') を使い、全角・半角、ひらがな・カタカナ、スペースを統一
str_replace(['ヶ', 'ケ'], 'ヶ', $normalized_keyword); で、「ケ」と「ヶ」を統一 ※市ヶ谷・市ケ谷、霞ケ関・霞ヶ関など
mb_strtolower($normalized_keyword); で 小文字に統一(例: ABC → abc)
Ajax検索で投稿タイトルのみを対象に検索
// Ajax検索で使用する関数
function get_filtered_terms() {
if (!isset($_POST['keyword']) || empty($_POST['keyword'])) {
wp_send_json_success(array('categories' => [], 'tags' => []));
}
global $wpdb;
$keyword = sanitize_text_field($_POST['keyword']); // キーワードのサニタイズ
// 検索キーワードを正規化
$normalized_keyword = normalize_search_keyword($keyword);
// 投稿のタイトルのみを対象に検索(固定ページを除外)
$post_ids = $wpdb->get_col($wpdb->prepare("
SELECT ID FROM $wpdb->posts
WHERE post_type = 'post'
AND post_status = 'publish'
AND post_title COLLATE utf8_unicode_ci LIKE %s
", '%' . $wpdb->esc_like($normalized_keyword) . '%'));
if (empty($post_ids)) {
wp_send_json_success(array('categories' => [], 'tags' => []));
}
// 関連するカテゴリーを取得
$categories = get_terms(array(
'taxonomy' => 'category',
'hide_empty' => false,
'object_ids' => $post_ids
));
// 関連するタグを取得
$tags = get_terms(array(
'taxonomy' => 'post_tag',
'hide_empty' => false,
'object_ids' => $post_ids
));
// JSONレスポンスを返す
wp_send_json_success(array(
'categories' => $categories,
'tags' => $tags
));
}
処理の詳細は以下の通り。
$_POST['keyword'] が存在しない、または空なら即終了
sanitize_text_field() でXSS対策(セキュリティ強化)
normalize_search_keyword() を適用し、半角・全角・ひらがな・カタカナを統一
投稿タイトル (post_title) で部分一致検索
検索結果に関連するカテゴリー・タグを取得し、JSONで返す
これにより、
検索対象が投稿タイトルのみに限定される
utf8_unicode_ci により、表記ゆれを考慮した検索が可能
検索結果に関連するカテゴリー・タグも取得
が可能となります。
Ajax リクエストを受け付ける処理
add_action('wp_ajax_get_filtered_terms', 'get_filtered_terms');
add_action('wp_ajax_nopriv_get_filtered_terms', 'get_filtered_terms');
処理の詳細は以下の通り。
wp_ajax_ でログインユーザー向けの Ajax リクエストを処理
wp_ajax_nopriv_ で非ログインユーザー向けの Ajax リクエストを処理
実装ポイント
子テーマの style.css をキャッシュ回避しながら適切に読み込む
ショートコード [refine_searchform] を作成し、検索フォームを簡単に挿入できるようにする
Ajax を使い、キーワードに基づいて関連するカテゴリー・タグを取得する
検索対象を「投稿タイトルのみに限定」し、固定ページを除外する。
興味がある方は一度試してみてください。(試す際は「style.css」「functions.php」の2ファイルのバックアップは忘れずに行う事と、既存の「searchform.php」は絶対に触らない事。)
今回は以上です。ここまで読んでいただきありがとうございました。
いいなと思ったら応援しよう!
