見出し画像

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>&nbsp;検索</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カラム表示になるようにレスポンシブ対応を行っています。

  1. カテゴリー・タグのプルダウンを2カラム(50:50)で配置

  2. 検索ボタンのデザインを統一し、サイトのブランドカラーを適用

  3. ホバー時に色が変わるアニメーションを追加

  4. 検索結果がない場合や未入力時にボタンを無効化し、灰色表示

  5. スマホ時(768px以下)では自動的に縦並びに変更し、操作しやすく

この実装により、PCでもスマホでも使いやすい検索フォームが完成します。
また、プルダウンのデザイン統一や、未入力時の無効化など、ユーザーの操作ミスを防ぐ工夫も取り入れています。

functions.phpの全体構造

この functions.php では、主に以下の機能を追加しています。

  1. 子テーマの style.css を適切に読み込む処理

  2. 検索フォームのショートコードを作成し、ページに簡単に挿入できるようにする処理

  3. 検索キーワードに基づいて関連するカテゴリー・タグを取得する Ajax 検索処理

  4. 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」は絶対に触らない事。)

今回は以上です。ここまで読んでいただきありがとうございました。

いいなと思ったら応援しよう!

わだっつ
よろしければ応援お願いします。いただきましたチップ、クリエイターとしての活動費として使わせていただきます。