WordPress 批量生成 SEO 摘要的 AI 工具實作解析

前言

這段 PHP 程式碼是一個 WordPress 管理後台的批量生成 SEO 摘要(excerpt)工具,利用 OpenAI API 自動為文章產生符合 SEO 需求的摘要文字。適合需要快速為大量文章補充或優化 meta description 的網站管理者與開發者,尤其是希望結合 AI 自動化生成摘要的工程師或自學者。

功能概述與設計

本工具提供一個後台管理頁面,允許使用者設定想要批量處理的文章類型(Post Type)、目標摘要字數、是否只生成空白摘要的文章,以及排除特定文章 ID。除了批量處理外,也支援單篇測試生成,方便調整摘要風格與長度。

管理頁面與設定儲存

透過 add_management_page 新增管理頁面,並以表單方式呈現設定選項。設定包含:

  • 選擇 Post Type
  • 目標字數(限制在 60 到 200 字)
  • 是否只對空白摘要文章生成
  • 全域排除文章 ID
  • 單篇測試生成功能

設定透過 admin_initPOST 請求保存,並使用 WordPress 的 nonce 機制與權限檢查確保安全。

單篇測試生成

使用者輸入文章 ID 後,前端透過 AJAX 呼叫 aiex_test_generate,後端取得文章內容後呼叫核心函式 aiex_generate_excerpt_for_post,使用 OpenAI API 產生摘要文字,回傳給前端顯示但不寫入資料庫。

批量生成與寫入

批量功能每次處理 10 篇文章,依據設定過濾排除文章(排除 ID、勾選略過 meta、已有摘要且設定只處理空白摘要)。

使用 WP_Query 依序取得文章,並呼叫相同的生成函式取得摘要,再用 wp_update_post 更新文章摘錄欄位。批量進度透過 transient 紀錄偏移量,避免重複處理。

互動式前端 UI

透過 JavaScript 實現批量開始、停止、進度條顯示與日誌紀錄,增強使用者體驗。批量完成後會提醒使用者停用程式碼片段並撤銷 API Key,避免安全風險。

核心生成邏輯解析

aiex_generate_excerpt_for_post 函式負責:

  1. 清理文章標題與內容,去除短碼與 HTML 標籤,限制輸入長度(最多 3000 字元)。
  2. 組合系統角色與使用者提示,明確要求生成符合 SEO 需求的繁體中文摘要,且限制字數範圍與格式。
  3. 透過 wp_remote_post 呼叫 OpenAI Chat Completion API,並處理回應。
  4. 對生成文字做長度硬限制與尾端標點修剪,確保摘要適合放入 WordPress excerpt 欄位。

此設計確保生成內容自然且不包含不必要的自我指涉或格式符號,符合 SEO 友善的描述需求。

實務應用與延伸

  • 可用於大型內容網站快速補齊缺失的 SEO 摘要,提升搜尋排名與點擊率。
  • 透過排除 ID 與單篇略過 meta,靈活控制哪些文章需要自動生成。
  • 可擴充支援多種語言或不同 AI 模型,依照帳號權限調整。
  • 前端 UI 可優化為更完整的進度管理與錯誤重試機制。
  • 注意 API Key 的安全性,建議執行後立即停用並撤銷 Key。

常見問題與注意事項

  • API Key 寫死風險:程式碼中硬編 API Key,使用完畢務必停用並撤銷,避免外洩。
  • 摘要覆蓋問題:設定中可選擇只對空白摘要生成,避免覆蓋手動編輯內容。
  • 批量效能:每次處理 10 篇避免伺服器負擔過重,可依需求調整批次大小。
  • OpenAI 回應限制:API 回應失敗或超時時會跳過該篇文章,需留意日誌。

完整程式碼

<?php
/**
* WPCode: AI SEO Excerpt 批量生成工具(可選 Post Type / 排除單篇 / 測試單篇 / 批量處理 + 互動 UI)
* ⚠️ 注意:此版本把 OpenAI API Key 寫死在程式內。跑完請立刻停用/刪除 snippet。
*/
if (!defined('ABSPATH')) exit;
const AIEX_OPT_KEY   = 'aiex_settings';
const AIEX_META_SKIP = '_aiex_skip_excerpt';
// ✅ 直接把你的 OpenAI Key 貼在這裡(跑完就刪掉/撤銷)
const AIEX_OPENAI_API_KEY = '';
// 可選:模型(依你帳號可用模型調整)
const AIEX_OPENAI_MODEL = 'gpt-4.1-mini';
/** =========================
* Admin Menu Page
* ========================= */
add_action('admin_menu', function () {
add_management_page(
'AI Excerpt 批量生成',
'AI Excerpt 批量生成',
'manage_options',
'aiex-bulk-excerpt',
'aiex_render_admin_page'
);
});
function aiex_render_admin_page() {
if (!current_user_can('manage_options')) wp_die('沒有權限');
$opts = get_option(AIEX_OPT_KEY, [
'post_type'     => 'post',
'exclude_ids'   => '',
'target_chars'  => 140,
'only_if_empty' => 1,
]);
$post_types = get_post_types(['public' => true], 'objects');
?>
<div class="wrap">
<h1>AI Excerpt 批量生成</h1>
<?php if (!AIEX_OPENAI_API_KEY || AIEX_OPENAI_API_KEY === 'PASTE_YOUR_OPENAI_KEY_HERE'): ?>
<div class="notice notice-error"><p>
請先把 <code>AIEX_OPENAI_API_KEY 改成你的 key(目前還是預設字串)。

⚠️ 你已把 API Key 寫在 WPCode snippet 內。跑完請立刻停用/刪除 snippet,並建議撤銷該 key。

批量互動

已寫入:0  已略過:0  已處理:0

測試結果

批量處理狀態


(function(){ const ajaxUrl = ""; const logEl = document.getElementById('aiex_log'); const outEl = document.getElementById('aiex_test_output'); const btnTest = document.getElementById('aiex_btn_test'); const btnBulk = document.getElementById('aiex_btn_bulk'); const btnStop = document.getElementById('aiex_btn_stop'); const bar = document.getElementById('aiex_progress_bar'); let running = false; let estimatedTotal = null; let totalUpdated = 0; let totalSkipped = 0; let totalProcessed = 0; function log(msg){ const now = new Date().toLocaleTimeString(); logEl.textContent += `[${now}] ${msg}\n`; logEl.scrollTop = logEl.scrollHeight; } function setStats(){ document.getElementById('stat_updated').textContent = totalUpdated; document.getElementById('stat_skipped').textContent = totalSkipped; document.getElementById('stat_processed').textContent = totalProcessed; } function resetUI(){ estimatedTotal = null; totalUpdated = 0; totalSkipped = 0; totalProcessed = 0; setStats(); bar.style.width = '0%'; bar.style.background = '#2563eb'; } window.onbeforeunload = function () { if (running) return '批量仍在進行中,確定要離開?'; }; btnTest.addEventListener('click', async function(){ outEl.value = ''; const testId = document.getElementById('aiex_test_post_id').value.trim(); if(!testId){ alert('請輸入文章 ID'); return; } log(`🔎 測試生成:post_id=${testId}`); const fd = new FormData(); fd.append('action', 'aiex_test_generate'); fd.append('nonce', ''); fd.append('post_id', testId); const res = await fetch(ajaxUrl, { method:'POST', credentials:'same-origin', body: fd }); const json = await res.json(); if(!json || !json.success){ log(`❌ 測試失敗:${json && json.data ? json.data : 'unknown error'}`); alert('測試失敗,請看狀態區'); return; } outEl.value = json.data.excerpt || ''; log('✅ 測試完成(未寫入)'); }); btnStop.addEventListener('click', function(){ running = false; btnStop.style.display = 'none'; btnBulk.disabled = false; log('⏹ 已停止批量'); }); btnBulk.addEventListener('click', async function(){ // 開始前確認互動 const postType = document.getElementById('aiex_post_type').value; const onlyEmpty = document.querySelector('input[name="only_if_empty"]').checked; const excludeIds = document.getElementById('aiex_exclude_ids').value.trim(); const excludeCount = excludeIds ? excludeIds.split(/,|\n/).map(s=>s.trim()).filter(Boolean).length : 0; const msg = `即將開始【批量寫入 excerpt】 Post Type:${postType} 只在 excerpt 空白時:${onlyEmpty ? '是' : '否'} 排除 ID 數量:${excludeCount} ⚠️ 此操作會直接寫入資料庫,確定要開始嗎?`; if (!confirm(msg)) { log('❌ 使用者取消批量'); return; } // reset UI resetUI(); running = true; btnBulk.disabled = true; btnStop.style.display = ''; log('🚀 開始批量處理(每次 10 篇)'); while(running){ const fd = new FormData(); fd.append('action', 'aiex_bulk_generate'); fd.append('nonce', ''); const res = await fetch(ajaxUrl, { method:'POST', credentials:'same-origin', body: fd }); const json = await res.json(); if(!json || !json.success){ log(`❌ 批量失敗:${json && json.data ? json.data : 'unknown error'}`); running = false; break; } const d = json.data; totalUpdated += (d.updated || 0); totalSkipped += (d.skipped || 0); totalProcessed += (d.processed || 0); setStats(); // 估算總量(第一次拿到 remaining 之後) if (!estimatedTotal && typeof d.remaining === 'number') { estimatedTotal = d.remaining + (d.processed || 0); } if (estimatedTotal && typeof d.remaining === 'number') { const doneCount = Math.max(0, estimatedTotal - d.remaining); const percent = Math.min(100, Math.round((doneCount / estimatedTotal) * 100)); bar.style.width = percent + '%'; } log(`本輪:處理=${d.processed} | 寫入=${d.updated} | 略過=${d.skipped} | 剩餘估計=${d.remaining}`); if(d.done){ log('🎉 ✅ 批量全部完成'); running = false; bar.style.width = '100%'; bar.style.background = '#16a34a'; alert(`批量完成 🎉 實際寫入:${totalUpdated} 略過:${totalSkipped} 處理總數:${totalProcessed} (建議:現在就停用/刪除 WPCode snippet,並撤銷此 API Key)`); // 符合你「跑完就結束」:完成後鎖住 btnBulk.disabled = true; btnBulk.textContent = '已完成(請停用 snippet)'; } // 小休息避免太猛 await new Promise(r => setTimeout(r, 400)); } btnStop.style.display = 'none'; if (btnBulk.textContent !== '已完成(請停用 snippet)') { btnBulk.disabled = false; } }); })(); $post_type, 'exclude_ids' => $exclude_ids, 'target_chars' => $target_chars, 'only_if_empty' => $only_if_empty, ]); add_action('admin_notices', function () { echo '

AI Excerpt 設定已儲存。

'; }); }); /** ========================= * Per-post skip meta box * ========================= */ add_action('add_meta_boxes', function () { $opts = get_option(AIEX_OPT_KEY, []); $pt = $opts['post_type'] ?? 'post'; if (!$pt || !post_type_exists($pt)) return; add_meta_box( 'aiex_skip_box', 'AI Excerpt', 'aiex_render_skip_metabox', $pt, 'side', 'default' ); }); function aiex_render_skip_metabox($post) { $val = get_post_meta($post->ID, AIEX_META_SKIP, true); wp_nonce_field('aiex_save_skip', 'aiex_skip_nonce'); ?> $excerpt]); }); /** ========================= * AJAX: Bulk generate (write) - 10 per call * ========================= */ add_action('wp_ajax_aiex_bulk_generate', function () { if (!current_user_can('manage_options')) wp_send_json_error('no_permission'); if (empty($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'aiex_ajax')) wp_send_json_error('bad_nonce'); $opts = get_option(AIEX_OPT_KEY, []); $pt = $opts['post_type'] ?? 'post'; $exclude_ids = aiex_parse_ids($opts['exclude_ids'] ?? ''); $target_chars = (int)($opts['target_chars'] ?? 140); $only_if_empty = !empty($opts['only_if_empty']); $batch = 10; $user_id = get_current_user_id(); $key = 'aiex_offset_' . $user_id . '_' . $pt; $offset = (int) get_transient($key); if ($offset $pt, 'post_status' => 'publish', 'posts_per_page' => $batch, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'all', ]); $processed = 0; $updated = 0; $skipped = 0; foreach ($q->posts as $post) { $processed++; if (in_array((int)$post->ID, $exclude_ids, true)) { $skipped++; continue; } if (get_post_meta($post->ID, AIEX_META_SKIP, true) === '1') { $skipped++; continue; } if ($only_if_empty && !empty(trim((string)$post->post_excerpt))) { $skipped++; continue; } $excerpt = aiex_generate_excerpt_for_post($post, $target_chars); if (!$excerpt) { $skipped++; continue; } wp_update_post([ 'ID' => $post->ID, 'post_excerpt' => $excerpt, ]); $updated++; } $new_offset = $offset + $processed; set_transient($key, $new_offset, HOUR_IN_SECONDS); $total = (int) $q->found_posts; $remaining = max(0, $total - $new_offset); $done = ($processed === 0) || ($new_offset >= $total); if ($done) delete_transient($key); wp_send_json_success([ 'processed' => $processed, 'updated' => $updated, 'skipped' => $skipped, 'remaining' => $remaining, 'done' => $done, ]); }); /** ========================= * Core: Generate excerpt (OpenAI) * ========================= */ function aiex_generate_excerpt_for_post($post, $target_chars = 140) { $title = wp_strip_all_tags((string)$post->post_title); $content = (string)$post->post_content; $content = strip_shortcodes($content); $content = wp_strip_all_tags($content); $content = preg_replace('/\s+/u', ' ', $content); $content = trim($content); if ($title === '' || $content === '') return ''; $max_input_chars = 3000; if (mb_strlen($content, 'UTF-8') > $max_input_chars) { $content = mb_substr($content, 0, $max_input_chars, 'UTF-8'); } if (!AIEX_OPENAI_API_KEY || AIEX_OPENAI_API_KEY === 'PASTE_YOUR_OPENAI_KEY_HERE') return ''; $api_key = AIEX_OPENAI_API_KEY; $model = (defined('AIEX_OPENAI_MODEL') && AIEX_OPENAI_MODEL) ? AIEX_OPENAI_MODEL : 'gpt-4.1-mini'; $endpoint = 'https://api.openai.com/v1/chat/completions'; $system = '你是一位資深 SEO 編輯。請根據提供的文章標題與內容,寫一段適合放在 SEO meta description / WordPress excerpt 的繁體中文摘要。'; $user = "標題:{$title}\n\n內容:{$content}\n\n要求:\n" . "- 只輸出 1 段純文字(不要加引號、不要列點、不要換行)\n" . "- 長度約 {$target_chars} 字(可上下浮動 15%)\n" . "- 要自然、具吸引力,包含關鍵資訊,不要誇大\n" . "- 不要出現「本文」「這篇文章」等自我指涉\n" . "- 不要包含網址、表情符號"; $body = [ 'model' => $model, 'temperature' => 0.5, 'messages' => [ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $user], ], ]; $response = wp_remote_post($endpoint, [ 'timeout' => 30, 'headers' => [ 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ], 'body' => wp_json_encode($body), ]); if (is_wp_error($response)) return ''; $code = wp_remote_retrieve_response_code($response); if ($code = 300) return ''; $raw = wp_remote_retrieve_body($response); $json = json_decode($raw, true); $text = $json['choices'][0]['message']['content'] ?? ''; $text = is_string($text) ? trim($text) : ''; $text = preg_replace('/\s+/u', ' ', $text); $text = trim($text); $hard_limit = (int) floor($target_chars * 1.2); if (mb_strlen($text, 'UTF-8') > $hard_limit) { $text = mb_substr($text, 0, $hard_limit, 'UTF-8'); $text = rtrim($text, " \t\n\r\0\x0B,,.。"); } return $text; } function aiex_parse_ids($text) { $text = (string)$text; if ($text === '') return []; $text = str_replace(["\n", "\r", "\t", ' '], ',', $text); $parts = array_filter(array_map('trim', explode(',', $text))); $ids = []; foreach ($parts as $p) { $n = (int)$p; if ($n > 0) $ids[] = $n; } return array_values(array_unique($ids)); }
標籤
分享
在 Facebook 分享 在 X (Twitter) 分享 在 Line 分享 在 Threads 分享