WordPress

✅【実践編】WordPressでプラグイン無しの絞り込み検索を実装する方法 – URLパラメータ方式のセキュアな実装 ✨

こんにちは、Web制作に関わるみなさん!👋 今回はWordPressサイトにおける「絞り込み検索機能」をプラグイン無しで実装する方法について、実践的なコードと共に解説します。特に、Ajaxを使わずURLパラメータを利用した、セキュアな実装方法をご紹介します!🔒

なぜAjaxではなくURLパラメータ方式なの?🤔

現場でよく「Ajaxで検索機能を実装して!」というリクエストがありますが、Ajaxは実装方法を誤るとXSS(クロスサイトスクリプティング)やCSRF(クロスサイトリクエストフォージェリ)などの脆弱性リスクが高まります。

一方、URLパラメータを使った検索方式には以下のメリットがあります:

🔹 検索条件がURLに表示されるのでブックマークや共有が可能
🔹 ブラウザの戻るボタンが正常に機能する
🔹 適切に実装すれば脆弱性リスクを低減できる
🔹 SEOにも優しい(検索エンジンがパラメータ付きURLをクロールしやすい)
🔹 JavaScriptが無効でも動作する(アクセシビリティの向上)

動的なUIが必要ない限り、URLパラメータ方式はシンプルで堅牢な選択肢です。

実装に必要なファイル📁

基本的に以下のファイルを編集または作成します:

  • functions.php – フィルター処理のためのカスタム関数を追加
  • searchform.php – 検索フォームのテンプレート(新規作成)
  • search.php – 検索結果表示ページ(既存を編集)

今回作成する絞り込み検索の機能プレビュー

以下のような機能を持つ絞り込み検索を一緒に実装していきます:

  • キーワード検索
  • カテゴリーによるフィルタリング
  • タグによるフィルタリング
  • 投稿日による絞り込み(1週間以内、1ヶ月以内など)
  • レスポンシブ対応のUIデザイン
  • 脆弱性対策を施したセキュアな実装

この記事で学べること

✅ WordPressのクエリAPIを使った堅牢な検索システムの構築方法
✅ セキュリティに配慮したURLパラメータ設計
✅ 検索フォームのカスタマイズとスタイリング
✅ カスタム投稿タイプやカスタムフィールドへの対応方法
✅ 実際の現場での応用例とカスタマイズポイント

プラグインに依存せず、パフォーマンスも考慮した軽量な検索機能を実装したい方にぴったりの内容です。

STEP 1: カスタム検索フォームの作成🔍

まずはテーマフォルダ直下に searchform.php を作成します。これがフィルター付き検索フォームになります。

<?php
// 現在のURLパラメータを取得(フォーム初期値として使用)
$current_category = isset($_GET['category']) ? sanitize_text_field($_GET['category']) : '';
$current_tag = isset($_GET['tag']) ? sanitize_text_field($_GET['tag']) : '';
$current_date = isset($_GET['date']) ? sanitize_text_field($_GET['date']) : '';
$search_query = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
?>

<form role="search" method="get" class="search-filter-form" action="<?php echo esc_url(home_url('/')); ?>">
    <div class="search-filter-container">
        <!-- キーワード検索フィールド -->
        <div class="search-filter-item">
            <label for="search-keyword">キーワード</label>
            <input type="text" id="search-keyword" name="s" value="<?php echo esc_attr($search_query); ?>" placeholder="キーワードを入力">
        </div>
        
        <!-- カテゴリー選択 -->
        <div class="search-filter-item">
            <label for="search-category">カテゴリー</label>
            <select id="search-category" name="category">
                <option value="">すべてのカテゴリー</option>
                <?php
                $categories = get_categories(array('hide_empty' => true));
                foreach ($categories as $category) {
                    $selected = ($current_category == $category->slug) ? 'selected' : '';
                    echo '<option value="' . esc_attr($category->slug) . '" ' . $selected . '>' . esc_html($category->name) . '</option>';
                }
                ?>
            </select>
        </div>
        
        <!-- タグ選択 -->
        <div class="search-filter-item">
            <label for="search-tag">タグ</label>
            <select id="search-tag" name="tag">
                <option value="">すべてのタグ</option>
                <?php
                $tags = get_tags(array('hide_empty' => true));
                foreach ($tags as $tag) {
                    $selected = ($current_tag == $tag->slug) ? 'selected' : '';
                    echo '<option value="' . esc_attr($tag->slug) . '" ' . $selected . '>' . esc_html($tag->name) . '</option>';
                }
                ?>
            </select>
        </div>
        
        <!-- 日付範囲選択 -->
        <div class="search-filter-item">
            <label for="search-date">投稿日</label>
            <select id="search-date" name="date">
                <option value="">すべての期間</option>
                <option value="7" <?php selected($current_date, '7'); ?>>1週間以内</option>
                <option value="30" <?php selected($current_date, '30'); ?>>1ヶ月以内</option>
                <option value="90" <?php selected($current_date, '90'); ?>>3ヶ月以内</option>
                <option value="365" <?php selected($current_date, '365'); ?>>1年以内</option>
            </select>
        </div>
        
        <!-- 検索ボタン -->
        <div class="search-filter-button">
            <button type="submit">検索する</button>
        </div>
    </div>
</form>

このコードでは以下のポイントに注目してください:

  • sanitize_text_field() で入力値をサニタイズしてXSS対策
  • selected() 関数で現在の選択状態を保持
  • esc_attr(), esc_html(), esc_url() でエスケープ処理を徹底

STEP 2: functions.phpにクエリ変更関数を追加🛠️

次に、functions.php に検索条件に基づいてメインクエリを変更する関数を追加します。あなたのテーマの functions.php に以下のコードを追加してください:

/**
 * 検索・絞り込み条件に基づいてメインクエリを変更する関数
 */
function custom_search_filter_query($query) {
    // メインクエリでない場合や管理画面では実行しない
    if (!$query->is_main_query() || is_admin()) {
        return $query;
    }
    
    // 検索ページまたはアーカイブページの場合のみ処理
    if ($query->is_search() || $query->is_archive()) {
        
        // カテゴリーフィルター
        if (isset($_GET['category']) && !empty($_GET['category'])) {
            $category = sanitize_text_field($_GET['category']);
            $query->set('category_name', $category);
        }
        
        // タグフィルター
        if (isset($_GET['tag']) && !empty($_GET['tag'])) {
            $tag = sanitize_text_field($_GET['tag']);
            $query->set('tag', $tag);
        }
        
        // 日付フィルター(n日以内)
        if (isset($_GET['date']) && !empty($_GET['date'])) {
            $days = intval($_GET['date']);
            if ($days > 0) {
                $date_query = array(
                    array(
                        'after' => date('Y-m-d', strtotime("-{$days} days")),
                        'inclusive' => true,
                    ),
                );
                $query->set('date_query', $date_query);
            }
        }
        
        // デフォルトの表示順を最新順に
        $query->set('orderby', 'date');
        $query->set('order', 'DESC');
    }
    
    return $query;
}
add_action('pre_get_posts', 'custom_search_filter_query');

このコードは pre_get_posts フックを使用して、WordPressのメインクエリが実行される前に検索条件を適用します。重要なポイント:

  • !$query->is_main_query() || is_admin() でメインクエリのみ処理(ウィジェットなどには影響しない)
  • sanitize_text_field()intval() でユーザー入力を適切にサニタイズ
  • date_query パラメータで期間指定を実装

STEP 3: 検索結果ページをカスタマイズ📋

search.php ファイルを編集して、検索フォームと現在の絞り込み条件の表示を追加します。 既存の search.php を以下のように変更します(または新規作成):

<?php get_header(); ?>

<div class="content-area">
    <main id="main" class="site-main">
        
        <header class="page-header">
            <h1 class="page-title">
                <?php printf(esc_html__('「%s」の検索結果', 'your-theme-text-domain'), get_search_query()); ?>
            </h1>
        </header>
        
        <!-- 検索フォームを表示 -->
        <?php get_template_part('searchform'); ?>
        
        <!-- 現在の絞り込み条件を表示 -->
        <?php
        $has_filter = false;
        $filter_text = '絞り込み条件: ';
        
        if (!empty($_GET['category'])) {
            $category = get_category_by_slug(sanitize_text_field($_GET['category']));
            if ($category) {
                $filter_text .= 'カテゴリー「' . esc_html($category->name) . '」 ';
                $has_filter = true;
            }
        }
        
        if (!empty($_GET['tag'])) {
            $tag = get_term_by('slug', sanitize_text_field($_GET['tag']), 'post_tag');
            if ($tag) {
                $filter_text .= 'タグ「' . esc_html($tag->name) . '」 ';
                $has_filter = true;
            }
        }
        
        if (!empty($_GET['date'])) {
            $days = intval($_GET['date']);
            $period = '';
            switch ($days) {
                case 7:
                    $period = '1週間以内';
                    break;
                case 30:
                    $period = '1ヶ月以内';
                    break;
                case 90:
                    $period = '3ヶ月以内';
                    break;
                case 365:
                    $period = '1年以内';
                    break;
            }
            if (!empty($period)) {
                $filter_text .= '期間「' . $period . '」 ';
                $has_filter = true;
            }
        }
        
        if ($has_filter) {
            echo '<div class="current-filters">' . esc_html($filter_text) . '</div>';
        }
        ?>
        
        <!-- 検索結果の表示 -->
        <?php if (have_posts()) : ?>
            
            <div class="search-results-container">
                <?php while (have_posts()) : the_post(); ?>
                    <article id="post-<?php the_ID(); ?>" <?php post_class('search-result-item'); ?>>
                        <header class="entry-header">
                            <h2 class="entry-title">
                                <a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
                            </h2>
                        </header>
                        
                        <?php if (has_post_thumbnail()) : ?>
                            <div class="entry-thumbnail">
                                <a href="<?php the_permalink(); ?>">
                                    <?php the_post_thumbnail('thumbnail'); ?>
                                </a>
                            </div>
                        <?php endif; ?>
                        
                        <div class="entry-summary">
                            <?php the_excerpt(); ?>
                        </div>
                        
                        <footer class="entry-meta">
                            <span class="posted-on">
                                <?php echo get_the_date(); ?>
                            </span>
                            <?php
                            $categories_list = get_the_category_list(', ');
                            if ($categories_list) {
                                echo '<span class="cat-links">カテゴリー: ' . $categories_list . '</span>';
                            }
                            
                            $tags_list = get_the_tag_list('', ', ');
                            if ($tags_list) {
                                echo '<span class="tag-links">タグ: ' . $tags_list . '</span>';
                            }
                            ?>
                        </footer>
                    </article>
                <?php endwhile; ?>
            </div>
            
            <?php the_posts_pagination(); ?>
            
        <?php else : ?>
            
            <div class="no-results">
                <h2>検索結果が見つかりませんでした</h2>
                <p>検索条件を変更して、再度お試しください。</p>
            </div>
            
        <?php endif; ?>
        
    </main>
    
    <?php get_sidebar(); ?>
</div>

<?php get_footer(); ?>

このコードでは以下のポイントに注目してください:

  • get_template_part('searchform') で作成した検索フォームを表示
  • 現在の絞り込み条件をわかりやすく表示
  • ページネーションの組み込み
  • 検索結果がない場合のメッセージ表示

STEP 4: スタイルを追加する💅

検索フォームと検索結果をスタイリングするためのCSSを追加します。functions.php に以下のコードを追加するか、テーマのスタイルシートに直接追加してください:

/**
 * 検索フィルター用のCSSを追加
 */
function add_search_filter_styles() {
    // インラインCSSの追加
    $custom_css = "
        /* 検索フォームのスタイル */
        .search-filter-form {
            margin-bottom: 30px;
            padding: 20px;
            background: #f8f9fa;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
        }
        
        .search-filter-container {
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
        }
        
        .search-filter-item {
            flex: 1 1 200px;
        }
        
        .search-filter-item label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            font-size: 14px;
            color: #333;
        }
        
        .search-filter-item input,
        .search-filter-item select {
            width: 100%;
            padding: 10px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        
        .search-filter-button {
            flex: 0 0 100%;
            text-align: right;
            margin-top: 10px;
        }
        
        .search-filter-button button {
            background: #0073aa;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            transition: background 0.3s ease;
        }
        
        .search-filter-button button:hover {
            background: #005177;
        }
        
        /* 現在の絞り込み条件表示 */
        .current-filters {
            margin-bottom: 20px;
            padding: 12px 15px;
            background: #e9f7fe;
            border-left: 4px solid #0073aa;
            font-size: 14px;
            line-height: 1.4;
        }
        
        /* 検索結果のスタイル */
        .search-result-item {
            margin-bottom: 30px;
            padding-bottom: 20px;
            border-bottom: 1px solid #eee;
        }
        
        .entry-title {
            margin-top: 0;
            margin-bottom: 10px;
            font-size: 1.4em;
        }
        
        .entry-title a {
            color: #333;
            text-decoration: none;
            transition: color 0.2s ease;
        }
        
        .entry-title a:hover {
            color: #0073aa;
        }
        
        .entry-thumbnail {
            float: left;
            margin-right: 20px;
            margin-bottom: 15px;
            max-width: 150px;
        }
        
        .entry-meta {
            font-size: 13px;
            color: #666;
            margin-top: 15px;
        }
        
        .entry-meta span {
            margin-right: 15px;
        }
        
        .no-results {
            padding: 30px;
            background: #f8f9fa;
            text-align: center;
            border-radius: 8px;
            margin-top: 20px;
        }
        
        /* レスポンシブ対応 */
        @media (max-width: 768px) {
            .search-filter-container {
                flex-direction: column;
            }
            
            .search-filter-item {
                flex: 0 0 100%;
            }
            
            .entry-thumbnail {
                float: none;
                display: block;
                margin: 0 0 15px 0;
                max-width: 100%;
            }
        }
    ";
    
    wp_add_inline_style('theme-style', $custom_css);
}
add_action('wp_enqueue_scripts', 'add_search_filter_styles');

このCSSはレスポンシブ対応も含んでおり、スマートフォンからタブレット、デスクトップまで美しく表示されます。CSSは主に以下の要素をスタイリングしています:

  • 検索フォームの全体的なレイアウト
  • 入力フィールドとセレクトボックス
  • 現在の絞り込み条件の表示
  • 検索結果のレイアウトとスタイル
  • モバイル対応のためのメディアクエリ

実際のユースケースとカスタマイズ例🚀

この基本実装をベースに、さまざまなプロジェクト要件に合わせてカスタマイズできます。以下に、実際の現場でよく要望される拡張例をいくつか紹介します:

1. カスタム投稿タイプの検索対応

特定のカスタム投稿タイプ(例: 製品、サービス、イベントなど)も検索対象にするには、以下のコードを functions.php の custom_search_filter_query 関数内に追加します:

// 検索対象に特定のカスタム投稿タイプを含める
if ($query->is_search()) {
    $post_types = array('post', 'page', 'product', 'event'); // 検索対象の投稿タイプ
    $query->set('post_type', $post_types);
}

2. カスタムフィールドによる絞り込み

WooCommerceの商品価格や不動産物件の面積など、カスタムフィールドによる絞り込みを追加するには:

// 価格範囲フィルター(カスタムフィールド検索の例)
$meta_query = array();

if (isset($_GET['price_min']) && !empty($_GET['price_min'])) {
    $price_min = intval($_GET['price_min']);
    $meta_query[] = array(
        'key' => 'price', // カスタムフィールドのキー
        'value' => $price_min,
        'type' => 'NUMERIC',
        'compare' => '>='
    );
}

if (isset($_GET['price_max']) && !empty($_GET['price_max'])) {
    $price_max = intval($_GET['price_max']);
    $meta_query[] = array(
        'key' => 'price',
        'value' => $price_max,
        'type' => 'NUMERIC',
        'compare' => '<='
    );
}

if (!empty($meta_query)) {
    $query->set('meta_query', $meta_query);
}

検索フォームにも対応する入力フィールドを追加します:

<!-- 価格範囲フィルター用のフォーム要素 -->
<div class="search-filter-item">
    <label for="price-min">最低価格</label>
    <input type="number" id="price-min" name="price_min" value="<?php echo isset($_GET['price_min']) ? intval($_GET['price_min']) : ''; ?>" min="0">
</div>
<div class="search-filter-item">
    <label for="price-max">最高価格</label>
    <input type="number" id="price-max" name="price_max" value="<?php echo isset($_GET['price_max']) ? intval($_GET['price_max']) : ''; ?>" min="0">
</div>

3. チェックボックスによる複数選択

複数のカテゴリーやタグを選択できるようにするには、セレクトボックスの代わりにチェックボックスを使用します:

<!-- 複数カテゴリー選択(チェックボックス) -->
<div class="search-filter-item">
    <label>カテゴリー(複数選択可)</label>
    <div class="checkbox-group">
        <?php
        $categories = get_categories(array('hide_empty' => true));
        $current_categories = isset($_GET['categories']) ? array_map('sanitize_text_field', $_GET['categories']) : array();
        
        foreach ($categories as $category) {
            $checked = in_array($category->slug, $current_categories) ? 'checked' : '';
            echo '<label class="checkbox-label">';
            echo '<input type="checkbox" name="categories[]" value="' . esc_attr($category->slug) . '" ' . $checked . '> ';
            echo esc_html($category->name);
            echo '</label>';
        }
        ?>
    </div>
</div>

そして、functions.php の検索処理部分も、配列パラメータに対応させます:

// 複数カテゴリー選択対応
if (isset($_GET['categories']) && !empty($_GET['categories']) && is_array($_GET['categories'])) {
    $categories = array_map('sanitize_text_field', $_GET['categories']);
    $tax_query[] = array(
        'taxonomy' => 'category',
        'field' => 'slug',
        'terms' => $categories,
        'operator' => 'IN',
    );
}

if (!empty($tax_query)) {
    $query->set('tax_query', $tax_query);
}

セキュリティ対策のポイント🔒

この実装では以下のセキュリティ対策が施されています:

  • 入力値のサニタイズ: すべてのユーザー入力に sanitize_text_field()intval() を使用
  • 出力のエスケープ: esc_html(), esc_attr(), esc_url() で適切にエスケープ処理
  • パラメータの検証: 不正なパラメータを事前にチェック
  • SQLインジェクション対策: $wpdb->prepare の間接的な使用(WordPressのクエリAPI利用)

さらに、より高度なセキュリティ対策として以下も検討できます:

// CSRF対策のためにnonce追加
function add_search_nonce() {
    wp_nonce_field('search_filter_nonce', 'search_nonce');
}

// nonceの検証
function verify_search_nonce() {
    if (isset($_GET['search_nonce']) && !wp_verify_nonce($_GET['search_nonce'], 'search_filter_nonce')) {
        wp_die('セキュリティチェックに失敗しました。');
    }
}

まとめ📝

今回ご紹介した方法では、以下のポイントを押さえています:

  • 🔸 プラグインに頼らずにシンプルで堅牢な検索機能を実装
  • 🔸 URLパラメータを使ったセキュアな実装でAjaxの脆弱性を回避
  • 🔸 徹底した入力検証とサニタイズでXSS脆弱性を防止
  • 🔸 レスポンシブ対応のスタイリングで様々なデバイスに対応
  • 🔸 拡張性の高い設計で様々なプロジェクト要件にカスタマイズ可能

この実装方法であれば、ユーザーにとって使いやすく、開発者にとっても管理しやすい検索システムが構築できます。また、URLにパラメータが含まれるため検索結果のシェアやブックマークも簡単です。

現場のプロジェクトに合わせて、ぜひカスタマイズしてみてください!何か質問があればコメント欄もしくはXでお気軽にどうぞ!

コーディング楽しんでいきましょう!👨‍💻👩‍💻

✉️ あなたのリクエスト教えてください!

「この記事わかりやすかった!」
「他にもこんな記事書いてほしい!」
そんな声を、ぜひXのDMで教えてください!😊📩

できるだけリクエストにお応えして、今後の記事作成に活かしていきます

▶️ Xもフォローしてもらえるとめちゃくちゃ嬉しいです!

hisa

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA