WordPress 自訂文章類型批次內容關鍵字取代工具實作

前言

在管理 WordPress 自訂文章類型(Custom Post Type)時,常會遇到需要批次替換文章內容或摘要中的特定字串的需求。手動逐篇修改不僅耗時,也容易出錯。這段程式碼示範如何在 WordPress 後台為名為 solution 的自訂文章類型新增一個子選單,提供一個簡單介面讓管理員輸入舊字串與新字串,並自動搜尋所有 solution 文章的內容與摘要進行批次替換。

這篇文章適合有基本 WordPress 開發經驗,想要了解如何擴充後台功能並操作自訂文章內容的工程師與自學者。

新增後台子選單

使用 add_action('admin_menu', ...) 來掛載函式,先判斷 solution 文章類型是否存在,避免錯誤。接著用 add_submenu_page 在 solution 文章列表下新增「內容關鍵字取代」的子選單,權限設定為可編輯文章的用戶。

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

    add_submenu_page(
        'edit.php?post_type=solution',
        '內容關鍵字取代',
        '內容關鍵字取代',
        'edit_posts',
        'solution-content-replace',
        'solution_content_replace_page'
    );
});

這段程式碼確保只有在 solution 文章類型存在時才新增選單,避免後台出現無效連結。

後台頁面與表單設計

solution_content_replace_page 函式負責渲染後台頁面。首先檢查使用者權限,防止未授權存取。頁面包含一個表單,讓使用者輸入「舊字串」與「新字串」,並使用 WordPress 的 nonce 機制防止 CSRF 攻擊。

if (!current_user_can('edit_posts')) {
    wp_die('沒有權限。');
}

// 表單中使用 wp_nonce_field 產生安全碼

表單送出後會執行字串替換邏輯。

批次搜尋與替換邏輯說明

當表單送出且 nonce 驗證通過後,程式會取得輸入的舊字串與新字串,並用 WP_Query 撈出所有 solution 文章的 ID。

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

接著逐篇文章讀取內容(post_content)與摘要(post_excerpt),利用 mb_strpos(若可用)或 strpos 檢查舊字串是否存在。若存在,則用 str_replace 替換。

if ($content_pos !== false) {
    $updated_content = str_replace($old, $new, $content);
    if ($updated_content !== $content) {
        $update_args['post_content'] = $updated_content;
        $status_parts[] = '內容';
    }
}

替換完成後,只有在內容或摘要確實有變動時才呼叫 wp_update_post 進行更新,避免不必要的資料庫寫入。

結果呈現與使用者體驗

執行完替換後,頁面會顯示替換結果列表,包括文章 ID、標題、替換狀態(內容、摘要或兩者)以及快速編輯連結,方便管理者後續檢查與微調。

若未輸入舊字串,會顯示警告;若找不到符合條件的文章,則提示資訊,提升使用者體驗。

實務應用與優化建議

此工具適合用於需要大量文字修正的場景,例如品牌名稱變更、產品名稱更新或錯字修正。未來可擴充功能,如:

  • 支援正規表達式替換
  • 限制替換範圍(例如只替換內容或摘要)
  • 增加替換前預覽功能
  • 加入替換記錄與回滾機制

此外,批次更新大量文章時,建議搭配分批處理避免伺服器超時。

常見問題與注意事項

  • 請確認使用者權限設定正確,避免無權限使用此工具。
  • 替換字串為空白時,會將舊字串刪除,請謹慎操作。
  • 使用 mb_strpos 可避免多字節字串判斷錯誤,若環境不支援會退回 strpos

完整程式碼


<?php
// 在 solution post type 底下新增子選單:內容關鍵字取代
add_action('admin_menu', function () {
    if (!post_type_exists('solution')) {
        return;
    }

    add_submenu_page(
        'edit.php?post_type=solution',
        '內容關鍵字取代',
        '內容關鍵字取代',
        'edit_posts',
        'solution-content-replace',
        'solution_content_replace_page'
    );
});

// 後台頁面
function solution_content_replace_page() {
    if (!current_user_can('edit_posts')) {
        wp_die('沒有權限。');
    }

    $old = '';
    $new = '';
    $results = [];
    $executed = false;

    if (isset($_POST['solution_replace_submit'])) {
        check_admin_referer('solution_replace_action', 'solution_replace_nonce');

        $old = isset($_POST['old_keyword']) ? sanitize_text_field($_POST['old_keyword']) : '';
        $new = isset($_POST['new_keyword']) ? sanitize_text_field($_POST['new_keyword']) : '';

        $executed = true;

        if ($old !== '') {
            $query = new WP_Query([
                'post_type'      => 'solution',
                'post_status'    => 'any',
                'posts_per_page' => -1,
                'fields'         => 'ids',
            ]);

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

                $content = $post->post_content;
                $excerpt = $post->post_excerpt;

                // 檢查舊字串是否存在(內容+摘要)
                if (function_exists('mb_strpos')) {
                    $content_pos = ($content !== '') ? mb_strpos($content, $old) : false;
                    $excerpt_pos = ($excerpt !== '') ? mb_strpos($excerpt, $old) : false;
                } else {
                    $content_pos = ($content !== '') ? strpos($content, $old) : false;
                    $excerpt_pos = ($excerpt !== '') ? strpos($excerpt, $old) : false;
                }

                if ($content_pos !== false || $excerpt_pos !== false) {
                    $update_args = [
                        'ID' => $post_id,
                    ];
                    $status_parts = [];

                    // 內容替換
                    if ($content_pos !== false) {
                        $updated_content = str_replace($old, $new, $content);
                        if ($updated_content !== $content) {
                            $update_args['post_content'] = $updated_content;
                            $status_parts[] = '內容';
                        }
                    }

                    // 摘要替換
                    if ($excerpt_pos !== false) {
                        $updated_excerpt = str_replace($old, $new, $excerpt);
                        if ($updated_excerpt !== $excerpt) {
                            $update_args['post_excerpt'] = $updated_excerpt;
                            $status_parts[] = '摘要';
                        }
                    }

                    // 真的有需要更新才存
                    if (!empty($status_parts)) {
                        wp_update_post($update_args);

                        $results[] = [
                            'id'     => $post_id,
                            'title'  => $post->post_title,
                            'status' => '已替換:' . implode('、', $status_parts),
                        ];
                    }
                }
            }
            wp_reset_postdata();
        }
    }
    ?>

    <div class="wrap">

<h1>Solution – 內容關鍵字取代</h1>

<p>此工具會掃描所有 <code>solution 文章的內容與摘要,將符合的字串批次替換。

<input type="text" id="old_keyword" name="old_keyword" class="regular-text" value="" placeholder="例如:舊字串">
<input type="text" id="new_keyword" name="new_keyword" class="regular-text" value="" placeholder="例如:新字串(可空白)">

取代結果

請輸入要搜尋的舊字串。

沒有任何文章的內容或摘要包含「」。

ID 標題 狀態 編輯連結
<a href="" class="button button-small">編輯
<?php }