WordPress 後台為自訂文章類型新增內容關鍵字搜尋功能

前言

在 WordPress 後台管理自訂文章類型(此處為 solution)時,若想快速搜尋文章內容中是否包含特定關鍵字,系統預設的搜尋功能可能無法精準或方便達成此需求。這段程式碼示範如何為 solution 文章類型新增一個後台子選單,提供內容關鍵字搜尋功能,適合需要管理大量文章並快速定位內容的開發者與網站管理員。

新增後台子選單

使用 admin_menu action,先檢查 solution 文章類型是否存在,避免錯誤。接著透過 add_submenu_page 在 solution 列表底下新增「內容關鍵字搜尋」子選單,設定權限為 edit_posts,確保只有具備編輯權限的使用者能操作。

add_action( 'admin_menu', function () {
    if ( ! post_type_exists( 'solution' ) ) {
        return;
    }

    add_submenu_page(
        'edit.php?post_type=solution',
        '內容關鍵字搜尋',
        '內容關鍵字搜尋',
        'edit_posts',
        'solution-content-search',
        'solution_content_search_page_render'
    );
} );

搜尋頁面渲染與表單處理

solution_content_search_page_render 函式負責渲染搜尋表單與結果頁面。首先檢查使用者權限,避免未授權存取。接著判斷是否有表單送出,並使用 WordPress 的 nonce 機制驗證安全性。

關鍵字透過 sanitize_text_fieldwp_unslash 清理,避免 XSS 與注入風險。

if ( isset( $_POST['solution_content_search_submit'] ) ) {
    check_admin_referer( 'solution_content_search_action', 'solution_content_search_nonce' );

    $keyword  = isset( $_POST['solution_keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['solution_keyword'] ) ) : '';
    $searched = true;

    if ( $keyword !== '' ) {
        // ...
    }
}

透過 WP_Query 及字串比對精確搜尋

WordPress 內建搜尋(s 參數)會搜尋標題、內容等欄位,但可能不夠精準。此處先用 WP_Query 以關鍵字搜尋取得可能相關文章 ID,然後逐篇讀取內容,使用 mb_stripos(若有)或 stripos 做不區分大小寫的字串比對,確保內容確實包含關鍵字。

$query = new WP_Query( array(
    'post_type'      => 'solution',
    'post_status'    => 'any',
    'posts_per_page' => -1,
    's'              => $keyword,
    'fields'         => 'ids',
) );

foreach ( $query->posts as $post_id ) {
    $post = get_post( $post_id );
    if ( ! $post ) {
        continue;
    }

    $content = wp_strip_all_tags( $post->post_content );

    if ( function_exists( 'mb_stripos' ) ) {
        $pos = mb_stripos( $content, $keyword );
    } else {
        $pos = stripos( $content, $keyword );
    }

    if ( $pos !== false ) {
        $results[] = $post_id;
    }
}
wp_reset_postdata();

搜尋結果呈現與操作介面

搜尋結果會以表格形式呈現,包含文章 ID、標題(連結至編輯頁面)、前台連結與編輯按鈕,方便快速查看與編輯。若無輸入關鍵字或找不到結果,會顯示相應提示訊息。

表單使用 WordPress 內建的 wp_nonce_fieldsubmit_button,確保安全與一致性。

實務應用與優化方向

此搜尋功能適合用於自訂文章類型管理,尤其是內容量大且需要精準定位文本的場景。未來可考慮加入分頁功能避免一次載入過多文章,或結合全文索引外掛提升搜尋效能。也可擴充搜尋範圍至自訂欄位或分類。

完整程式碼

<?php
// 在「solution」這個 post type 底下新增一個後台子選單:內容關鍵字搜尋
add_action( 'admin_menu', function () {
    // 確保有這個 post type
    if ( ! post_type_exists( 'solution' ) ) {
        return;
    }

    add_submenu_page(
        'edit.php?post_type=solution',          // 父層 (solution 列表底下)
        '內容關鍵字搜尋',                        // 頁面標題
        '內容關鍵字搜尋',                        // 左側選單名稱
        'edit_posts',                           // 權限
        'solution-content-search',              // slug
        'solution_content_search_page_render'   // callback
    );
} );

// 後台頁面:渲染搜尋表單與結果
function solution_content_search_page_render() {
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_die( '沒有權限檢視此頁面。' );
    }

    $keyword  = '';
    $results  = array();
    $searched = false;

    // 處理表單送出
    if ( isset( $_POST['solution_content_search_submit'] ) ) {
        check_admin_referer( 'solution_content_search_action', 'solution_content_search_nonce' );

        $keyword  = isset( $_POST['solution_keyword'] ) ? sanitize_text_field( wp_unslash( $_POST['solution_keyword'] ) ) : '';
        $searched = true;

        if ( $keyword !== '' ) {
            // 先用 WP_Query 找出可能包含關鍵字的文章 (全文搜尋)
            $query = new WP_Query( array(
                'post_type'      => 'solution',
                'post_status'    => 'any',
                'posts_per_page' => -1,
                's'              => $keyword,
                'fields'         => 'ids',
            ) );

            if ( $query->have_posts() ) {
                foreach ( $query->posts as $post_id ) {
                    $post = get_post( $post_id );
                    if ( ! $post ) {
                        continue;
                    }

                    // 從內容中做字串檢查(確保 content 真的有包含關鍵字)
                    $content = wp_strip_all_tags( $post->post_content );

                    if ( function_exists( 'mb_stripos' ) ) {
                        $pos = mb_stripos( $content, $keyword );
                    } else {
                        $pos = stripos( $content, $keyword );
                    }

                    if ( $pos !== false ) {
                        $results[] = $post_id;
                    }
                }
            }
            wp_reset_postdata();
        }
    }

    ?>
    <div class="wrap">

<h1>Solution 內容關鍵字搜尋</h1>

<p>輸入一個關鍵字,系統會掃描 &lt;code&gt;solution&lt;/code&gt;
 文章類型的內容(content),只要有包含該字串,就列出標題與編輯連結。</p>

        <form method="post" style="margin-top: 1em; margin-bottom: 2em;">
            <?php wp_nonce_field( 'solution_content_search_action', 'solution_content_search_nonce' ); ?>

            <table class="form-table" role="presentation">

<tr>
                    <th scope="row">
                        <label for="solution_keyword">關鍵字字串</label>
                    </th>

<td>
                        <input
                            type="text"
                            id="solution_keyword"
                            name="solution_keyword"
                            class="regular-text"
                            value="<?php echo esc_attr( $keyword ); ?>"
                            placeholder="例如:API、某段程式碼、特定中文句子"
                        />
                    </td>
                </tr>
            </table>

            <?php submit_button( '開始搜尋', 'primary', 'solution_content_search_submit' ); ?>
        </form>

        <?php if ( $searched ) : ?>

<h2>搜尋結果</h2>

            <?php if ( $keyword === '' ) : ?>
                <div class="notice notice-warning"><p>請輸入關鍵字。</p></div>
            <?php elseif ( empty( $results ) ) : ?>
                <div class="notice notice-info"><p>沒有找到內容包含「<?php echo esc_html( $keyword ); ?>」的 solution 文章。</p></div>
            <?php else : ?>

<p>找到 <?php echo esc_html( count( $results ) ); ?> 篇文章,內容中包含「<?php echo esc_html( $keyword ); ?>」。</p>

                <table class="widefat fixed striped">

<thead>

<tr>
                            <th scope="col" style="width: 50px;">ID</th>
                            <th scope="col">標題</th>
                            <th scope="col" style="width: 200px;">前台連結</th>
                            <th scope="col" style="width: 150px;">編輯</th>
                        </tr>
                    </thead>

<tbody>
                    <?php foreach ( $results as $post_id ) :
                        $title     = get_the_title( $post_id );
                        $view_link = get_permalink( $post_id );
                        $edit_link = get_edit_post_link( $post_id, '' );
                    ?>

<tr>

<td><?php echo esc_html( $post_id ); ?></td>

<td>
                                <?php if ( $edit_link ) : ?>
                                    <a href="<?php echo esc_url( $edit_link ); ?>">
                                        <?php echo esc_html( $title ); ?>
                                    </a>
                                <?php else : ?>
                                    <?php echo esc_html( $title ); ?>
                                <?php endif; ?>
                            </td>

<td>
                                <?php if ( $view_link ) : ?>
                                    <a href="<?php echo esc_url( $view_link ); ?>" target="_blank" rel="noopener noreferrer">
                                        檢視
                                    </a>
                                <?php else : ?>
                                    -
                                <?php endif; ?>
                            </td>

<td>
                                <?php if ( $edit_link ) : ?>
                                    <a href="<?php echo esc_url( $edit_link ); ?>" class="button button-small">
                                        編輯文章
                                    </a>
                                <?php else : ?>
                                    -
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>
        <?php endif; ?>
    </div>
    <?php
}