<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>自訂文章類型 &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/%E8%87%AA%E8%A8%82%E6%96%87%E7%AB%A0%E9%A1%9E%E5%9E%8B/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Tue, 30 Dec 2025 15:03:34 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://piglife.tw/wp-content/uploads/2017/10/cropped-logo-1-32x32.png</url>
	<title>自訂文章類型 &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>WordPress 自訂文章類型批次內容關鍵字取代工具實作</title>
		<link>https://piglife.tw/technical-notes/wordpress-solution-content-replace-2/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-solution-content-replace-2/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Mon, 29 Dec 2025 22:21:12 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[後台擴充]]></category>
		<category><![CDATA[批次替換]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-solution-content-replace-2/</guid>

					<description><![CDATA[介紹如何在 WordPress 自訂文章類型 solution 的後台新增批次內容關鍵字替換工具，透...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在管理 WordPress 自訂文章類型（Custom Post Type）時，常會遇到需要批次替換文章內容或摘要中的特定字串的需求。手動逐篇修改不僅耗時，也容易出錯。這段程式碼示範如何在 WordPress 後台為名為 solution 的自訂文章類型新增一個子選單，提供一個簡單介面讓管理員輸入舊字串與新字串，並自動搜尋所有 solution 文章的內容與摘要進行批次替換。</p>
<p>這篇文章適合有基本 WordPress 開發經驗，想要了解如何擴充後台功能並操作自訂文章內容的工程師與自學者。</p>
<h2 class="wp-block-heading">新增後台子選單</h2>
<p>使用 <code>add_action(&#039;admin_menu&#039;, ...)</code> 來掛載函式，先判斷 solution 文章類型是否存在，避免錯誤。接著用 <code>add_submenu_page</code> 在 solution 文章列表下新增「內容關鍵字取代」的子選單，權限設定為可編輯文章的用戶。</p>
<pre><code class="lang-php language-php php">add_action(&#039;admin_menu&#039;, function () {
    if (!post_type_exists(&#039;solution&#039;)) {
        return;
    }

    add_submenu_page(
        &#039;edit.php?post_type=solution&#039;,
        &#039;內容關鍵字取代&#039;,
        &#039;內容關鍵字取代&#039;,
        &#039;edit_posts&#039;,
        &#039;solution-content-replace&#039;,
        &#039;solution_content_replace_page&#039;
    );
});</code></pre>
<p>這段程式碼確保只有在 solution 文章類型存在時才新增選單，避免後台出現無效連結。</p>
<h2 class="wp-block-heading">後台頁面與表單設計</h2>
<p><code>solution_content_replace_page</code> 函式負責渲染後台頁面。首先檢查使用者權限，防止未授權存取。頁面包含一個表單，讓使用者輸入「舊字串」與「新字串」，並使用 WordPress 的 nonce 機制防止 CSRF 攻擊。</p>
<pre><code class="lang-php language-php php">if (!current_user_can(&#039;edit_posts&#039;)) {
    wp_die(&#039;沒有權限。&#039;);
}

// 表單中使用 wp_nonce_field 產生安全碼</code></pre>
<p>表單送出後會執行字串替換邏輯。</p>
<h2 class="wp-block-heading">批次搜尋與替換邏輯說明</h2>
<p>當表單送出且 nonce 驗證通過後，程式會取得輸入的舊字串與新字串，並用 <code>WP_Query</code> 撈出所有 solution 文章的 ID。</p>
<pre><code class="lang-php language-php php">$query = new WP_Query([
    &#039;post_type&#039;      =&gt; &#039;solution&#039;,
    &#039;post_status&#039;    =&gt; &#039;any&#039;,
    &#039;posts_per_page&#039; =&gt; -1,
    &#039;fields&#039;         =&gt; &#039;ids&#039;,
]);</code></pre>
<p>接著逐篇文章讀取內容（<code>post_content</code>）與摘要（<code>post_excerpt</code>），利用 <code>mb_strpos</code>（若可用）或 <code>strpos</code> 檢查舊字串是否存在。若存在，則用 <code>str_replace</code> 替換。</p>
<pre><code class="lang-php language-php php">if ($content_pos !== false) {
    $updated_content = str_replace($old, $new, $content);
    if ($updated_content !== $content) {
        $update_args[&#039;post_content&#039;] = $updated_content;
        $status_parts[] = &#039;內容&#039;;
    }
}</code></pre>
<p>替換完成後，只有在內容或摘要確實有變動時才呼叫 <code>wp_update_post</code> 進行更新，避免不必要的資料庫寫入。</p>
<h2 class="wp-block-heading">結果呈現與使用者體驗</h2>
<p>執行完替換後，頁面會顯示替換結果列表，包括文章 ID、標題、替換狀態（內容、摘要或兩者）以及快速編輯連結，方便管理者後續檢查與微調。</p>
<p>若未輸入舊字串，會顯示警告；若找不到符合條件的文章，則提示資訊，提升使用者體驗。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<p>此工具適合用於需要大量文字修正的場景，例如品牌名稱變更、產品名稱更新或錯字修正。未來可擴充功能，如：</p>
<ul>
<li>支援正規表達式替換</li>
<li>限制替換範圍（例如只替換內容或摘要）</li>
<li>增加替換前預覽功能</li>
<li>加入替換記錄與回滾機制</li>
</ul>
<p>此外，批次更新大量文章時，建議搭配分批處理避免伺服器超時。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>請確認使用者權限設定正確，避免無權限使用此工具。</li>
<li>替換字串為空白時，會將舊字串刪除，請謹慎操作。</li>
<li>使用 <code>mb_strpos</code> 可避免多字節字串判斷錯誤，若環境不支援會退回 <code>strpos</code>。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">
&lt;?php
// 在 solution post type 底下新增子選單：內容關鍵字取代
add_action(&#039;admin_menu&#039;, function () {
    if (!post_type_exists(&#039;solution&#039;)) {
        return;
    }

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

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

    $old = &#039;&#039;;
    $new = &#039;&#039;;
    $results = [];
    $executed = false;

    if (isset($_POST[&#039;solution_replace_submit&#039;])) {
        check_admin_referer(&#039;solution_replace_action&#039;, &#039;solution_replace_nonce&#039;);

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

        $executed = true;

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

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

                $content = $post-&gt;post_content;
                $excerpt = $post-&gt;post_excerpt;

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

                if ($content_pos !== false || $excerpt_pos !== false) {
                    $update_args = [
                        &#039;ID&#039; =&gt; $post_id,
                    ];
                    $status_parts = [];

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

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

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

                        $results[] = [
                            &#039;id&#039;     =&gt; $post_id,
                            &#039;title&#039;  =&gt; $post-&gt;post_title,
                            &#039;status&#039; =&gt; &#039;已替換：&#039; . implode(&#039;、&#039;, $status_parts),
                        ];
                    }
                }
            }
            wp_reset_postdata();
        }
    }
    ?&gt;

&lt;h1&gt;Solution &ndash; 內容關鍵字取代&lt;/h1&gt;

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

        
            

            <table class="form-table">

<tr>

<th><label for="old_keyword">要搜尋的字（舊字串）</label></th>

<td>
                        &lt;input type=&quot;text&quot; id=&quot;old_keyword&quot; name=&quot;old_keyword&quot; class=&quot;regular-text&quot;
                               value=&quot;"
                               placeholder="例如：舊字串"&gt;
                    </td>
                </tr>

<tr>

<th><label for="new_keyword">替換成（新字串）</label></th>

<td>
                        &lt;input type=&quot;text&quot; id=&quot;new_keyword&quot; name=&quot;new_keyword&quot; class=&quot;regular-text&quot;
                               value=&quot;"
                               placeholder="例如：新字串（可空白）"&gt;
                    </td>
                </tr>
            </table>

            
        

        

<h3>取代結果</h3>

            
                <div class="notice notice-warning"><p>請輸入要搜尋的舊字串。</p></div>

            
                <div class="notice notice-info"><p>沒有任何文章的內容或摘要包含「」。</p></div>

            
                <table class="widefat striped">

<thead>

<tr>

<th>ID</th>

<th>標題</th>

<th>狀態</th>

<th>編輯連結</th>
                    </tr>
                    </thead>

<tbody>
                    

<tr>

<td></td>

<td></td>

<td></td>

<td>&lt;a href=&quot;" class="button button-small"&gt;編輯</a></td>
                        </tr>
                    
                    </tbody>
                </table>
            

        

    &lt;?php
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-solution-content-replace-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 後台為自訂文章類型新增內容關鍵字搜尋功能</title>
		<link>https://piglife.tw/technical-notes/wordpress-solution-content-search/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-solution-content-search/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Mon, 22 Dec 2025 22:20:38 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[wp_query]]></category>
		<category><![CDATA[內容關鍵字搜尋]]></category>
		<category><![CDATA[後台搜尋]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-solution-content-search/</guid>

					<description><![CDATA[介紹如何在 WordPress 自訂文章類型 solution 的後台新增內容關鍵字搜尋功能，透過 ...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 後台管理自訂文章類型（此處為 solution）時，若想快速搜尋文章內容中是否包含特定關鍵字，系統預設的搜尋功能可能無法精準或方便達成此需求。這段程式碼示範如何為 solution 文章類型新增一個後台子選單，提供內容關鍵字搜尋功能，適合需要管理大量文章並快速定位內容的開發者與網站管理員。</p>
<h2 class="wp-block-heading">新增後台子選單</h2>
<p>使用 <code>admin_menu</code> action，先檢查 solution 文章類型是否存在，避免錯誤。接著透過 <code>add_submenu_page</code> 在 solution 列表底下新增「內容關鍵字搜尋」子選單，設定權限為 <code>edit_posts</code>，確保只有具備編輯權限的使用者能操作。</p>
<pre><code class="lang-php language-php php">add_action( &#039;admin_menu&#039;, function () {
    if ( ! post_type_exists( &#039;solution&#039; ) ) {
        return;
    }

    add_submenu_page(
        &#039;edit.php?post_type=solution&#039;,
        &#039;內容關鍵字搜尋&#039;,
        &#039;內容關鍵字搜尋&#039;,
        &#039;edit_posts&#039;,
        &#039;solution-content-search&#039;,
        &#039;solution_content_search_page_render&#039;
    );
} );</code></pre>
<h2 class="wp-block-heading">搜尋頁面渲染與表單處理</h2>
<p><code>solution_content_search_page_render</code> 函式負責渲染搜尋表單與結果頁面。首先檢查使用者權限，避免未授權存取。接著判斷是否有表單送出，並使用 WordPress 的 nonce 機制驗證安全性。</p>
<p>關鍵字透過 <code>sanitize_text_field</code> 與 <code>wp_unslash</code> 清理，避免 XSS 與注入風險。</p>
<pre><code class="lang-php language-php php">if ( isset( $_POST[&#039;solution_content_search_submit&#039;] ) ) {
    check_admin_referer( &#039;solution_content_search_action&#039;, &#039;solution_content_search_nonce&#039; );

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

    if ( $keyword !== &#039;&#039; ) {
        // ...
    }
}</code></pre>
<h2 class="wp-block-heading">透過 WP_Query 及字串比對精確搜尋</h2>
<p>WordPress 內建搜尋（<code>s</code> 參數）會搜尋標題、內容等欄位，但可能不夠精準。此處先用 <code>WP_Query</code> 以關鍵字搜尋取得可能相關文章 ID，然後逐篇讀取內容，使用 <code>mb_stripos</code>（若有）或 <code>stripos</code> 做不區分大小寫的字串比對，確保內容確實包含關鍵字。</p>
<pre><code class="lang-php language-php php">$query = new WP_Query( array(
    &#039;post_type&#039;      =&gt; &#039;solution&#039;,
    &#039;post_status&#039;    =&gt; &#039;any&#039;,
    &#039;posts_per_page&#039; =&gt; -1,
    &#039;s&#039;              =&gt; $keyword,
    &#039;fields&#039;         =&gt; &#039;ids&#039;,
) );

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

    $content = wp_strip_all_tags( $post-&gt;post_content );

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

    if ( $pos !== false ) {
        $results[] = $post_id;
    }
}
wp_reset_postdata();</code></pre>
<h2 class="wp-block-heading">搜尋結果呈現與操作介面</h2>
<p>搜尋結果會以表格形式呈現，包含文章 ID、標題（連結至編輯頁面）、前台連結與編輯按鈕，方便快速查看與編輯。若無輸入關鍵字或找不到結果，會顯示相應提示訊息。</p>
<p>表單使用 WordPress 內建的 <code>wp_nonce_field</code> 與 <code>submit_button</code>，確保安全與一致性。</p>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<p>此搜尋功能適合用於自訂文章類型管理，尤其是內容量大且需要精準定位文本的場景。未來可考慮加入分頁功能避免一次載入過多文章，或結合全文索引外掛提升搜尋效能。也可擴充搜尋範圍至自訂欄位或分類。</p>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
// 在「solution」這個 post type 底下新增一個後台子選單：內容關鍵字搜尋
add_action( &#039;admin_menu&#039;, function () {
    // 確保有這個 post type
    if ( ! post_type_exists( &#039;solution&#039; ) ) {
        return;
    }

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

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

    $keyword  = &#039;&#039;;
    $results  = array();
    $searched = false;

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

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

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

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

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

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

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

    ?&gt;
    &lt;div class=&quot;wrap&quot;&gt;

&lt;h1&gt;Solution 內容關鍵字搜尋&lt;/h1&gt;

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

        &lt;form method=&quot;post&quot; style=&quot;margin-top: 1em; margin-bottom: 2em;&quot;&gt;
            &lt;?php wp_nonce_field( &#039;solution_content_search_action&#039;, &#039;solution_content_search_nonce&#039; ); ?&gt;

            &lt;table class=&quot;form-table&quot; role=&quot;presentation&quot;&gt;

&lt;tr&gt;
                    &lt;th scope=&quot;row&quot;&gt;
                        &lt;label for=&quot;solution_keyword&quot;&gt;關鍵字字串&lt;/label&gt;
                    &lt;/th&gt;

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

            &lt;?php submit_button( &#039;開始搜尋&#039;, &#039;primary&#039;, &#039;solution_content_search_submit&#039; ); ?&gt;
        &lt;/form&gt;

        &lt;?php if ( $searched ) : ?&gt;

&lt;h2&gt;搜尋結果&lt;/h2&gt;

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

&lt;p&gt;找到 &lt;?php echo esc_html( count( $results ) ); ?&gt; 篇文章，內容中包含「&lt;?php echo esc_html( $keyword ); ?&gt;」。&lt;/p&gt;

                &lt;table class=&quot;widefat fixed striped&quot;&gt;

&lt;thead&gt;

&lt;tr&gt;
                            &lt;th scope=&quot;col&quot; style=&quot;width: 50px;&quot;&gt;ID&lt;/th&gt;
                            &lt;th scope=&quot;col&quot;&gt;標題&lt;/th&gt;
                            &lt;th scope=&quot;col&quot; style=&quot;width: 200px;&quot;&gt;前台連結&lt;/th&gt;
                            &lt;th scope=&quot;col&quot; style=&quot;width: 150px;&quot;&gt;編輯&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;

&lt;tbody&gt;
                    &lt;?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, &#039;&#039; );
                    ?&gt;

&lt;tr&gt;

&lt;td&gt;&lt;?php echo esc_html( $post_id ); ?&gt;&lt;/td&gt;

&lt;td&gt;
                                &lt;?php if ( $edit_link ) : ?&gt;
                                    &lt;a href=&quot;&lt;?php echo esc_url( $edit_link ); ?&gt;&quot;&gt;
                                        &lt;?php echo esc_html( $title ); ?&gt;
                                    &lt;/a&gt;
                                &lt;?php else : ?&gt;
                                    &lt;?php echo esc_html( $title ); ?&gt;
                                &lt;?php endif; ?&gt;
                            &lt;/td&gt;

&lt;td&gt;
                                &lt;?php if ( $view_link ) : ?&gt;
                                    &lt;a href=&quot;&lt;?php echo esc_url( $view_link ); ?&gt;&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;
                                        檢視
                                    &lt;/a&gt;
                                &lt;?php else : ?&gt;
                                    -
                                &lt;?php endif; ?&gt;
                            &lt;/td&gt;

&lt;td&gt;
                                &lt;?php if ( $edit_link ) : ?&gt;
                                    &lt;a href=&quot;&lt;?php echo esc_url( $edit_link ); ?&gt;&quot; class=&quot;button button-small&quot;&gt;
                                        編輯文章
                                    &lt;/a&gt;
                                &lt;?php else : ?&gt;
                                    -
                                &lt;?php endif; ?&gt;
                            &lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;?php endforeach; ?&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
            &lt;?php endif; ?&gt;
        &lt;?php endif; ?&gt;
    &lt;/div&gt;
    &lt;?php
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-solution-content-search/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 自訂文章類型批次內容關鍵字取代工具實作</title>
		<link>https://piglife.tw/technical-notes/wordpress-solution-content-replace/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-solution-content-replace/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Sun, 21 Dec 2025 22:20:56 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[後台功能]]></category>
		<category><![CDATA[批次替換]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-solution-content-replace/</guid>

					<description><![CDATA[介紹如何在 WordPress 自訂文章類型 solution 中新增後台子選單，實作批次搜尋並替換...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 後台管理自訂文章類型（Custom Post Type）時，有時需要快速批次更新文章內容中的特定字串。這段程式碼示範如何在名為 <code>solution</code> 的自訂文章類型新增一個子選單，提供後台管理者輸入舊字串與新字串，並批次替換所有相關文章內容中的文字。適合有基本 WordPress 開發經驗，想要自訂後台功能以提升內容維護效率的工程師或自學者。</p>
<h2 class="wp-block-heading">新增子選單介面</h2>
<p>利用 <code>admin_menu</code> action 新增子選單，並限定該選單只在 <code>solution</code> 文章類型下顯示。權限設定為 <code>edit_posts</code>，確保只有有編輯文章權限的使用者能操作。</p>
<pre><code class="lang-php language-php php">add_action(&#039;admin_menu&#039;, function () {
    if (!post_type_exists(&#039;solution&#039;)) {
        return;
    }

    add_submenu_page(
        &#039;edit.php?post_type=solution&#039;,
        &#039;內容關鍵字取代&#039;,
        &#039;內容關鍵字取代&#039;,
        &#039;edit_posts&#039;,
        &#039;solution-content-replace&#039;,
        &#039;solution_content_replace_page&#039;
    );
});</code></pre>
<p>這段程式碼確保子選單只會在 <code>solution</code> 文章類型的管理頁面出現，並且點擊後會呼叫 <code>solution_content_replace_page</code> 函式來渲染頁面。</p>
<h2 class="wp-block-heading">後台頁面與表單處理</h2>
<p><code>solution_content_replace_page</code> 函式負責顯示輸入表單與處理替換邏輯。首先檢查使用者權限，避免未授權存取。接著使用 WordPress 的 Nonce 機制保障表單安全。</p>
<pre><code class="lang-php language-php php">if (!current_user_can(&#039;edit_posts&#039;)) {
    wp_die(&#039;沒有權限。&#039;);
}

if (isset($_POST[&#039;solution_replace_submit&#039;])) {
    check_admin_referer(&#039;solution_replace_action&#039;, &#039;solution_replace_nonce&#039;);

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

    // 進行批次替換
}</code></pre>
<p>使用者輸入的舊字串與新字串會經過 <code>sanitize_text_field</code> 清理，避免 XSS 或其他安全問題。</p>
<h2 class="wp-block-heading">批次搜尋與替換邏輯</h2>
<p>當舊字串不為空時，使用 <code>WP_Query</code> 撈取所有 <code>solution</code> 文章的 ID，並逐篇讀取內容。若文章內容包含舊字串，則使用 <code>str_replace</code> 替換後，呼叫 <code>wp_update_post</code> 更新文章。</p>
<pre><code class="lang-php language-php php">$query = new WP_Query([
    &#039;post_type&#039;      =&gt; &#039;solution&#039;,
    &#039;post_status&#039;    =&gt; &#039;any&#039;,
    &#039;posts_per_page&#039; =&gt; -1,
    &#039;fields&#039;         =&gt; &#039;ids&#039;,
]);

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

    $content = $post-&gt;post_content;

    if (mb_strpos($content, $old) !== false) {
        $updated_content = str_replace($old, $new, $content);

        wp_update_post([
            &#039;ID&#039;           =&gt; $post_id,
            &#039;post_content&#039; =&gt; $updated_content
        ]);

        $results[] = [
            &#039;id&#039;     =&gt; $post_id,
            &#039;title&#039;  =&gt; $post-&gt;post_title,
            &#039;status&#039; =&gt; &#039;已替換&#039;
        ];
    }
}
wp_reset_postdata();</code></pre>
<h2 class="wp-block-heading">結果呈現與使用者體驗</h2>
<p>頁面會根據執行狀態顯示不同訊息：</p>
<ul>
<li>若未輸入舊字串，提醒使用者必須填寫。</li>
<li>若找不到包含舊字串的文章，顯示資訊提示。</li>
<li>若有替換成功的文章，列出文章 ID、標題、狀態與編輯連結，方便管理者後續檢視。</li>
</ul>
<p>這樣的設計讓使用者能清楚知道批次操作的結果，並快速跳轉編輯。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<p>此功能適合內容量大且需定期文字修正的網站，如產品說明、技術文件等。未來可擴充：</p>
<ul>
<li>支援正則表達式替換，提高靈活度。</li>
<li>加入替換前後內容差異預覽，降低誤替換風險。</li>
<li>加入分頁或批次處理，避免大量文章一次更新造成伺服器負擔。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>請確認使用者權限設定正確，避免誤用。</li>
<li>替換操作無法還原，建議先備份資料庫。</li>
<li>文字替換為純字串，不支援 HTML 或多語系複雜處理。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
// 在 solution post type 底下新增子選單：內容關鍵字取代
add_action(&#039;admin_menu&#039;, function () {
    if (!post_type_exists(&#039;solution&#039;)) {
        return;
    }

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

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

    $old = &#039;&#039;;
    $new = &#039;&#039;;
    $results = [];
    $executed = false;

    if (isset($_POST[&#039;solution_replace_submit&#039;])) {
        check_admin_referer(&#039;solution_replace_action&#039;, &#039;solution_replace_nonce&#039;);

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

        $executed = true;

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

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

                $content = $post-&gt;post_content;

                // 檢查舊字串是否存在
                if (mb_strpos($content, $old) !== false) {
                    // 替換
                    $updated_content = str_replace($old, $new, $content);

                    // 更新文章
                    wp_update_post([
                        &#039;ID&#039;           =&gt; $post_id,
                        &#039;post_content&#039; =&gt; $updated_content
                    ]);

                    $results[] = [
                        &#039;id&#039;     =&gt; $post_id,
                        &#039;title&#039;  =&gt; $post-&gt;post_title,
                        &#039;status&#039; =&gt; &#039;已替換&#039;
                    ];
                }
            }
            wp_reset_postdata();
        }
    }
    ?&gt;

&lt;div class=&quot;wrap&quot;&gt;

&lt;h2&gt;Solution &ndash; 內容關鍵字取代&lt;/h2&gt;

        &lt;form method=&quot;post&quot;&gt;
            &lt;?php wp_nonce_field(&#039;solution_replace_action&#039;, &#039;solution_replace_nonce&#039;); ?&gt;

            &lt;table class=&quot;form-table&quot;&gt;

&lt;tr&gt;

&lt;th&gt;&lt;label for=&quot;old_keyword&quot;&gt;要搜尋的字（舊字串）&lt;/label&gt;&lt;/th&gt;

&lt;td&gt;
                        &lt;input type=&quot;text&quot; id=&quot;old_keyword&quot; name=&quot;old_keyword&quot; class=&quot;regular-text&quot;
                               value=&quot;&lt;?php echo esc_attr($old); ?&gt;&quot;
                               placeholder=&quot;例如：舊字串&quot;&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;

&lt;tr&gt;

&lt;th&gt;&lt;label for=&quot;new_keyword&quot;&gt;替換成（新字串）&lt;/label&gt;&lt;/th&gt;

&lt;td&gt;
                        &lt;input type=&quot;text&quot; id=&quot;new_keyword&quot; name=&quot;new_keyword&quot; class=&quot;regular-text&quot;
                               value=&quot;&lt;?php echo esc_attr($new); ?&gt;&quot;
                               placeholder=&quot;例如：新字串（可空白）&quot;&gt;
                    &lt;/td&gt;
                &lt;/tr&gt;
            &lt;/table&gt;

            &lt;?php submit_button(&#039;開始批次取代&#039;, &#039;primary&#039;, &#039;solution_replace_submit&#039;); ?&gt;
        &lt;/form&gt;

        &lt;?php if ($executed): ?&gt;

&lt;h2&gt;取代結果&lt;/h2&gt;

            &lt;?php if ($old === &#039;&#039;): ?&gt;
                &lt;div class=&quot;notice notice-warning&quot;&gt;&lt;p&gt;請輸入要搜尋的舊字串。&lt;/p&gt;&lt;/div&gt;

            &lt;?php elseif (empty($results)): ?&gt;
                &lt;div class=&quot;notice notice-info&quot;&gt;&lt;p&gt;沒有任何文章包含「&lt;?php echo esc_html($old); ?&gt;」。&lt;/p&gt;&lt;/div&gt;

            &lt;?php else: ?&gt;
                &lt;table class=&quot;widefat striped&quot;&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;ID&lt;/th&gt;

&lt;th&gt;標題&lt;/th&gt;

&lt;th&gt;狀態&lt;/th&gt;

&lt;th&gt;編輯連結&lt;/th&gt;
                    &lt;/tr&gt;
                    &lt;/thead&gt;

&lt;tbody&gt;
                    &lt;?php foreach ($results as $r): ?&gt;

&lt;tr&gt;

&lt;td&gt;&lt;?php echo esc_html($r[&#039;id&#039;]); ?&gt;&lt;/td&gt;

&lt;td&gt;&lt;?php echo esc_html($r[&#039;title&#039;]); ?&gt;&lt;/td&gt;

&lt;td&gt;&lt;?php echo esc_html($r[&#039;status&#039;]); ?&gt;&lt;/td&gt;

&lt;td&gt;&lt;a href=&quot;&lt;?php echo get_edit_post_link($r[&#039;id&#039;]); ?&gt;&quot; class=&quot;button button-small&quot;&gt;編輯&lt;/a&gt;&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;?php endforeach; ?&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
            &lt;?php endif; ?&gt;

        &lt;?php endif; ?&gt;
    &lt;/div&gt;

    &lt;?php
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-solution-content-replace/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 Polylang 官方流程批量複製 WordPress 文章翻譯</title>
		<link>https://piglife.tw/technical-notes/polylang-bulk-copy-translations/</link>
					<comments>https://piglife.tw/technical-notes/polylang-bulk-copy-translations/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Sat, 20 Dec 2025 22:21:10 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[acf]]></category>
		<category><![CDATA[Polylang]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[wp_insert_post]]></category>
		<category><![CDATA[多語系]]></category>
		<category><![CDATA[翻譯複製]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/polylang-bulk-copy-translations/</guid>

					<description><![CDATA[介紹如何使用 Polylang 官方 API 批量複製 WordPress 自訂文章類型的內容，建立...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>這段 PHP 程式碼是為了解決 WordPress 使用 Polylang 多語系外掛時，如何批量將特定文章類型的內容從一種語言複製到另一種語言，並建立正確的翻譯關聯。適合需要管理大量多語系內容的網站管理員或開發者，尤其是想自動化翻譯文章複製流程的技術人員。</p>
<h2 class="wp-block-heading">Polylang 官方 API 的使用背景</h2>
<p>Polylang 提供了多個官方函式來管理文章語言與翻譯關聯，如 <code>pll_set_post_language</code> 設定文章語言，<code>pll_save_post_translations</code> 儲存翻譯群組，這是官方推薦的做法，能確保多語系資料完整且一致。</p>
<h2 class="wp-block-heading">後台介面設計與操作流程</h2>
<h3 class="wp-block-heading">後台子選單與頁面</h3>
<p>程式碼透過 <code>add_submenu_page</code> 在自訂文章類型「investor_relations」的管理頁面下新增子選單，提供單筆測試與批量複製兩種操作介面。使用者可選擇目標語言（目前限制為英文與日文），並執行對應動作。</p>
<h3 class="wp-block-heading">權限與安全檢查</h3>
<p>頁面載入時會檢查使用者是否有 <code>manage_options</code> 權限，並確認 Polylang API 函式存在，避免外掛未啟用或權限不足導致錯誤。</p>
<h2 class="wp-block-heading">核心功能：複製文章並建立翻譯關聯</h2>
<h3 class="wp-block-heading">1. 確認來源文章及語言設定</h3>
<p>函式 <code>ir_clone_post_official</code> 會先取得來源文章，並用 <code>pll_set_post_language</code> 強制設定來源文章語言，確保語言標記正確。</p>
<pre><code class="lang-php language-php php">ir_set_lang_official( $source_id, $source_lang );</code></pre>
<h3 class="wp-block-heading">2. 檢查目標語言翻譯是否已存在</h3>
<p>利用 <code>pll_get_post_translations</code> 取得該文章的所有語言版本，若目標語言已存在翻譯文章，則跳過建立，避免重複。</p>
<h3 class="wp-block-heading">3. 建立新文章（翻譯版本）</h3>
<p>使用 WordPress 官方函式 <code>wp_insert_post</code> 複製文章內容與屬性，並設定新文章的語言為目標語言。</p>
<pre><code class="lang-php language-php php">$new_id = wp_insert_post([...]);
ir_set_lang_official( $new_id, $target_lang );</code></pre>
<h3 class="wp-block-heading">4. 儲存翻譯關聯</h3>
<p>將來源文章與新文章的語言關聯資料更新至 Polylang，確保前台語言切換功能正常。</p>
<pre><code class="lang-php language-php php">$translations[ $target_lang ] = $new_id;
pll_save_post_translations( $translations );</code></pre>
<h3 class="wp-block-heading">5. 複製分類法與特色圖片</h3>
<p>排除語言分類法後，複製其他自訂分類法的關聯，並複製特色圖片，保持內容一致性。</p>
<h3 class="wp-block-heading">6. 複製 ACF 自訂欄位資料</h3>
<p>若有安裝 Advanced Custom Fields 外掛，會將所有欄位資料一併複製，包含上傳檔案，避免資料遺失。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li><strong>備份資料庫</strong>：執行批量複製前務必備份，避免誤操作導致資料錯亂。</li>
<li><strong>單筆測試</strong>：先用單筆測試功能確保流程正常，再進行批量操作。</li>
<li><strong>擴充語言</strong>：可依需求新增允許的目標語言陣列。</li>
<li><strong>錯誤處理</strong>：可加強錯誤回報與日誌紀錄，方便除錯與維護。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>Polylang API 函式不存在時，需確認外掛是否啟用。</li>
<li>複製文章時，若有其他外掛影響文章資料，可能需要額外處理。</li>
<li>語言分類法不應被複製，以免覆寫 Polylang 的語言設定。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
/**
 * Investor Relations 單語言批量翻譯複製工具（最終版）
 * - 完全走 Polylang 官方流程：
 *   wp_insert_post + pll_set_post_language + pll_save_post_translations
 * - 自動跳過「已經有目標語言翻譯」的文章
 */

define( &#039;IR_SOURCE_LANG&#039;, &#039;zh&#039; );                  // 來源語言：中文
define( &#039;IR_POST_TYPE&#039;, &#039;investor_relations&#039; );    // Post type：investor_relations

/* =========================
 * 共用：設定語言（官方 API）
 * ========================= */
function ir_set_lang_official( $post_id, $lang_code ) {
    if ( function_exists( &#039;pll_set_post_language&#039; ) ) {
        pll_set_post_language( $post_id, $lang_code );
    }
}

/* =========================
 * 後台子選單
 * ========================= */
add_action( &#039;admin_menu&#039;, function () {
    add_submenu_page(
        &#039;edit.php?post_type=&#039; . IR_POST_TYPE,
        &#039;Bulk Copy Translations (Official)&#039;,
        &#039;Bulk Copy Translations (Official)&#039;,
        &#039;manage_options&#039;,
        &#039;ir-bulk-translations-official&#039;,
        &#039;ir_bulk_translations_official_page&#039;
    );
});

/* =========================
 * 後台頁面 + 控制流程
 * ========================= */
function ir_bulk_translations_official_page() {

    // 允許的目標語言（依照 Polylang 的 code）
    $allowed_target_langs = [ &#039;en&#039;, &#039;ja&#039; ];

    if ( ! current_user_can( &#039;manage_options&#039; ) ) {
        wp_die( &#039;沒有權限。&#039; );
    }

    if ( ! function_exists( &#039;pll_set_post_language&#039; ) || ! function_exists( &#039;pll_save_post_translations&#039; ) ) {
        echo &#039;&lt;div class=&quot;notice notice-error&quot;&gt;&lt;p&gt;Polylang API 不存在，請確認外掛是否啟用。&lt;/p&gt;&lt;/div&gt;&#039;;
        return;
    }

    // 目前選擇的目標語言（預設 en）
    $current_target = isset( $_POST[&#039;ir_target_lang&#039;] )
        ? sanitize_text_field( $_POST[&#039;ir_target_lang&#039;] )
        : &#039;en&#039;;

    if ( ! in_array( $current_target, $allowed_target_langs, true ) ) {
        $current_target = &#039;en&#039;;
    }

    echo &#039;
&lt;h1&gt;Investor Relations 翻譯複製工具（官方流程版）&lt;/h1&gt;&#039;;
    echo &#039;
&lt;p&gt;來源語言固定為：&lt;code&gt;&#039; . esc_html( IR_SOURCE_LANG ) . &#039;</code></p>';
    echo '<p style="color:red"><strong>⚠️ 執行前務必先備份資料庫！</strong></p>';

    echo '
.ir-lang-select{margin-left:8px;}';

    /* ------------ 單筆測試表單 ------------- */
    echo '
<p>單筆文章測試</p>';
    echo '';
    wp_nonce_field( 'ir_single_test_official' );
    echo '<label>輸入文章 ID：<input type="number" name="ir_test_id"></label>';

    echo '<span class="ir-lang-select">目標語言：';
    echo '';
    foreach ( $allowed_target_langs as $code ) {
        printf(
            '%s',
            esc_attr( $code ),
            selected( $current_target, $code, false ),
            esc_html( strtoupper( $code ) )
        );
    }
    echo '</span> ';

    submit_button( '測試複製此文章', 'secondary', 'ir_single_test', false );
    echo '';

    /* ------------ 批次表單 ------------- */
    echo '
<p>批量複製所有中文文章</p>';
    echo '';
    wp_nonce_field( 'ir_bulk_official' );

    echo '<span>目標語言：';
    echo '';
    foreach ( $allowed_target_langs as $code ) {
        printf(
            '%s',
            esc_attr( $code ),
            selected( $current_target, $code, false ),
            esc_html( strtoupper( $code ) )
        );
    }
    echo '</span> ';

    submit_button( '執行批次複製（請先單筆測試）', 'primary', 'ir_bulk_run', false );
    echo '';

    /* ------------ 執行動作 ------------- */

    // 單筆
    if ( isset( $_POST['ir_single_test'] ) ) {
        check_admin_referer( 'ir_single_test_official' );
        $post_id = isset( $_POST['ir_test_id'] ) ? intval( $_POST['ir_test_id'] ) : 0;
        $target  = isset( $_POST['ir_target_lang'] ) ? sanitize_text_field( $_POST['ir_target_lang'] ) : 'en';
        ir_run_single_copy_official( $post_id, $target );
    }

    // 批次
    if ( isset( $_POST['ir_bulk_run'] ) ) {
        check_admin_referer( 'ir_bulk_official' );
        $target = isset( $_POST['ir_target_lang'] ) ? sanitize_text_field( $_POST['ir_target_lang'] ) : 'en';
        ir_run_bulk_copy_official( $target );
    }

}</code></pre>
<pre><code class="lang-php language-php php">function ir_run_single_copy_official( $post_id, $target_lang ) {
    echo &#039;
&lt;h3&gt;單筆測試結果&lt;/h3&gt;&#039;;

    if ( ! $post_id || ! get_post( $post_id ) ) {
        echo &#039;&lt;p style=&quot;color:red;&quot;&gt;找不到文章 ID：&#039; . esc_html( $post_id ) . &#039;&lt;/p&gt;&#039;;
        return;
    }

    $report = ir_clone_post_official( $post_id, $target_lang );

    echo &#039;&lt;pre style=&quot;background:white; padding:10px; border:1px solid #ccc;&quot;&gt;&#039;;
    echo esc_html( implode( &quot;\n&quot;, $report ) );
    echo &#039;&lt;/pre&gt;&#039;;
}

/* =========================
 * 批量處理所有中文文章（官方流程）
 * ========================= */
function ir_run_bulk_copy_official( $target_lang ) {
    $posts = get_posts( [
        &#039;post_type&#039;      =&gt; IR_POST_TYPE,
        &#039;posts_per_page&#039; =&gt; -1,
        &#039;post_status&#039;    =&gt; &#039;any&#039;,
        &#039;lang&#039;           =&gt; IR_SOURCE_LANG, // Polylang 的語言 query var
        &#039;fields&#039;         =&gt; &#039;ids&#039;,
    ] );

    echo &#039;
&lt;h2&gt;批次執行結果（目標語言：&#039; . esc_html( strtoupper( $target_lang ) ) . &#039;）&lt;/h2&gt;&#039;;

    if ( empty( $posts ) ) {
        echo &#039;
&lt;p&gt;沒有找到來源語言（&#039; . esc_html( IR_SOURCE_LANG ) . &#039;）的文章。&lt;/p&gt;&#039;;
        return;
    }

    $output = [];

    foreach ( $posts as $post_id ) {
        $report  = ir_clone_post_official( $post_id, $target_lang );
        $output  = array_merge( $output, $report );
    }

    echo &#039;&lt;pre style=&quot;background:white; max-height:450px; overflow:auto; padding:10px; border:1px solid #ccc;&quot;&gt;&#039;;
    echo esc_html( implode( &quot;\n&quot;, $output ) );
    echo &#039;&lt;/pre&gt;&#039;;
}

/* =========================
 * 核心：官方流程複製一篇文章 &rarr; 目標語言翻譯
 * ========================= */
function ir_clone_post_official( $source_id, $target_lang ) {
    $report = [];

    $source_post = get_post( $source_id );
    if ( ! $source_post ) {
        $report[] = &quot;❌ 原文 {$source_id} 不存在&quot;;
        return $report;
    }

    $source_lang = IR_SOURCE_LANG;

    // Step 1) 先確保原文語言正確
    ir_set_lang_official( $source_id, $source_lang );
    $report[] = &quot;Step 1) 設定原文語言：{$source_id} &rarr; {$source_lang}&quot;;

    // 取得現有翻譯 group
    $translations = function_exists( &#039;pll_get_post_translations&#039; )
        ? pll_get_post_translations( $source_id )
        : [];

    if ( empty( $translations[ $source_lang ] ) ) {
        $translations[ $source_lang ] = $source_id;
    }

    // ✅ 若已存在該語言翻譯 &rarr; 自動跳過
    if ( ! empty( $translations[ $target_lang ] ) ) {
        $existing = $translations[ $target_lang ];
        $report[] = &quot;⚠️ 原文 {$source_id} 已有 {$target_lang} 翻譯（ID {$existing}），跳過建立新文章。&quot;;

        // 做個語言 debug
        $lang_debug = [];
        foreach ( $translations as $code =&gt; $pid ) {
            $lang_debug[] = $code . &#039;:&#039; . $pid . &#039;(&#039; . pll_get_post_language( $pid ) . &#039;)&#039;;
        }
        $report[] = &#039;🧪 目前語言狀態：&#039; . implode( &#039;, &#039;, $lang_debug );

        return $report;
    }

    // Step 2) 使用 wp_insert_post 建立新文章（官方建議做法）
    $new_id = wp_insert_post( [
        &#039;post_type&#039;      =&gt; $source_post-&gt;post_type,
        &#039;post_status&#039;    =&gt; $source_post-&gt;post_status,
        &#039;post_title&#039;     =&gt; $source_post-&gt;post_title,
        &#039;post_content&#039;   =&gt; $source_post-&gt;post_content,
        &#039;post_excerpt&#039;   =&gt; $source_post-&gt;post_excerpt,
        &#039;post_author&#039;    =&gt; $source_post-&gt;post_author,
        &#039;post_date&#039;      =&gt; $source_post-&gt;post_date,
        &#039;post_date_gmt&#039;  =&gt; $source_post-&gt;post_date_gmt,
        &#039;menu_order&#039;     =&gt; $source_post-&gt;menu_order,
    ], true );

    if ( is_wp_error( $new_id ) ) {
        $report[] = &quot;❌ wp_insert_post 錯誤：&quot; . $new_id-&gt;get_error_message();
        return $report;
    }
    $report[] = &quot;Step 2) 建立新文章：{$new_id}&quot;;

    // Step 3) 官方建議：先設定新文章語言
    ir_set_lang_official( $new_id, $target_lang );
    $report[] = &quot;Step 3) 設定新文章語言：{$new_id} &rarr; {$target_lang}&quot;;

    // Step 4) 官方建議：建立翻譯關聯（保留既有語言）
    $translations[ $target_lang ] = $new_id;
    pll_save_post_translations( $translations );
    $report[] = &quot;Step 4) 儲存翻譯關聯：&quot; . json_encode( $translations );

    // Step 5) 複製 taxonomy（排除語言 taxonomy，避免覆寫語言）
    $taxes = get_object_taxonomies( IR_POST_TYPE );
    $taxes = array_diff( $taxes, [ &#039;language&#039;, &#039;pll_language&#039; ] );

    foreach ( $taxes as $tax ) {
        $terms = wp_get_object_terms( $source_id, $tax, [ &#039;fields&#039; =&gt; &#039;ids&#039; ] );
        if ( ! is_wp_error( $terms ) ) {
            wp_set_object_terms( $new_id, $terms, $tax, false );
        }
    }

    // Step 6) 複製特色圖片
    $thumb = get_post_thumbnail_id( $source_id );
    if ( $thumb ) {
        set_post_thumbnail( $new_id, $thumb );
    }

    // Step 7) 複製 ACF 欄位（包含上傳檔案）
    if ( function_exists( &#039;get_field_objects&#039; ) &amp;&amp; function_exists( &#039;update_field&#039; ) ) {
        $fields = get_field_objects( $source_id );
        if ( $fields ) {
            foreach ( $fields as $field ) {
                update_field( $field[&#039;key&#039;], $field[&#039;value&#039;], $new_id );
            }
        }
    }

    // 最後：實際語言檢查
    $src_lang_real = pll_get_post_language( $source_id );
    $new_lang_real = pll_get_post_language( $new_id );

    $report[] = &quot;🧪 實際語言檢查：原文 {$source_id} 語言：{$src_lang_real}；新文 {$new_id} 語言：{$new_lang_real}&quot;;
    $report[] = &quot;✅ 完成：原文 {$source_id} &rarr; {$target_lang} 翻譯 {$new_id}&quot;;

    return $report;
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/polylang-bulk-copy-translations/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 會員註冊時自動建立自訂文章類型實作筆記</title>
		<link>https://piglife.tw/technical-notes/wordpress-auto-create-cpt/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-auto-create-cpt/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Thu, 20 Feb 2025 05:45:16 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[acf]]></category>
		<category><![CDATA[user_register]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[wp_insert_post]]></category>
		<category><![CDATA[會員註冊]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/?p=493</guid>

					<description><![CDATA[說明如何在 WordPress 會員註冊時，自動建立自訂文章類型 member_info，並設定預設...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 網站中，常見需求是當新會員註冊時，自動建立一篇專屬於該會員的自訂文章類型（Custom Post Type, CPT），例如會員介紹頁面。本文適合具備基本 WordPress 開發經驗的工程師或自學者，說明如何利用 <code>user_register</code> Hook 自動建立 <code>member_info</code> 文章，並設定內容與 ACF 欄位。</p>
<h2 class="wp-block-heading">註冊 member_info 自訂文章類型</h2>
<p>首先，必須在主題的 <code>functions.php</code> 中註冊 <code>member_info</code> 這個自訂文章類型。以下程式碼會在 WordPress 初始化時執行：</p>
<pre><code class="lang-php language-php php">function register_member_info_post_type() {
  $args = array(
    &#039;labels&#039; =&gt; array(
      &#039;name&#039; =&gt; &#039;會員資訊&#039;,
      &#039;singular_name&#039; =&gt; &#039;會員資訊&#039;
    ),
    &#039;public&#039; =&gt; true,
    &#039;has_archive&#039; =&gt; true,
    &#039;supports&#039; =&gt; array(&#039;title&#039;, &#039;editor&#039;, &#039;author&#039;),
    &#039;rewrite&#039; =&gt; array(&#039;slug&#039; =&gt; &#039;member-info&#039;),
  );
  register_post_type(&#039;member_info&#039;, $args);
}
add_action(&#039;init&#039;, &#039;register_member_info_post_type&#039;);</code></pre>
<p>此段程式碼會建立一個公開且有存檔頁的 CPT，並支援標題、內容編輯器與作者欄位。</p>
<h2 class="wp-block-heading">自動建立會員資訊文章</h2>
<p>接著，利用 <code>user_register</code> Hook 監聽新會員註冊事件，當會員註冊時自動建立一篇 <code>member_info</code> 文章，並填入預設內容與 ACF 欄位：</p>
<pre><code class="lang-php language-php php">function ks_create_member_info_post($user_id) {
  $user = get_userdata($user_id);
  $display_name = $user-&gt;display_name;
  $post_title = $display_name;

  $post_content = &#039;

  &lt;h3 class=&quot;wp-block-heading&quot;&gt;&lt;strong&gt;經歷&lt;/strong&gt;（公開資訊）&lt;/h3&gt;

&lt;p&gt;公開資訊&lt;/p&gt;

  &lt;h3 class=&quot;wp-block-heading&quot;&gt;&lt;strong&gt;專長&lt;/strong&gt;（公開資訊）&lt;/h3&gt;

&lt;p&gt;公開資訊&lt;/p&gt;
  &#039;;

  $post_data = array(
    &#039;post_title&#039; =&gt; $post_title,
    &#039;post_content&#039; =&gt; $post_content,
    &#039;post_status&#039; =&gt; &#039;publish&#039;,
    &#039;post_author&#039; =&gt; $user_id,
    &#039;post_type&#039; =&gt; &#039;member_info&#039;,
  );

  $post_id = wp_insert_post($post_data);

  if ($post_id) {
    update_field(&#039;member_content&#039;, &#039;登入會員限定內容&#039;, $post_id);
    update_field(&#039;member_current_position&#039;, &#039;目前任職&#039;, $post_id);
    update_field(&#039;member_experience&#039;, &#039;（公開資訊區）&lt;br&gt;信箱、網站、學歷、學號？&#039;, $post_id);
  }
}
add_action(&#039;user_register&#039;, &#039;ks_create_member_info_post&#039;);</code></pre>
<p>此函式會取得新會員的顯示名稱作為文章標題，並用 Gutenberg 格式設定文章內容。若有安裝 Advanced Custom Fields，則會填入指定欄位的預設值。</p>
<h2 class="wp-block-heading">程式碼說明</h2>
<ul>
<li><code>user_register</code> Hook：會員註冊時觸發，執行自動建立文章函式。</li>
<li><code>get_userdata($user_id)</code>：取得會員資料。</li>
<li><code>wp_insert_post()</code>：建立自訂文章並回傳文章 ID。</li>
<li>Gutenberg 內容格式：使用區塊編輯器標記語法，方便未來編輯。</li>
<li>ACF 欄位設定：透過 <code>update_field()</code> 填入預設值，需先安裝並啟用 ACF 外掛。</li>
</ul>
<h2 class="wp-block-heading">確保程式運作正確</h2>
<ul>
<li>確認 <code>member_info</code> CPT 已成功註冊並可在後台管理。</li>
<li>確認 Advanced Custom Fields 外掛已啟用，且欄位名稱正確。</li>
<li>測試新會員註冊流程，並檢查「會員資訊」文章是否自動產生。</li>
</ul>
<h2 class="wp-block-heading">延伸應用與常見坑</h2>
<ul>
<li>若需更多欄位，可在 ACF 中擴充並同步更新 <code>update_field()</code>。</li>
<li>若會員資料需同步更新文章，可監聽 <code>profile_update</code> Hook。</li>
<li>注意文章內容格式需符合 Gutenberg 區塊語法，避免前端顯示異常。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-text language-text text">function register_member_info_post_type() {
  $args = array(
    &#039;labels&#039; =&gt; array(
      &#039;name&#039; =&gt; &#039;會員資訊&#039;,
      &#039;singular_name&#039; =&gt; &#039;會員資訊&#039;
    ),
    &#039;public&#039; =&gt; true,
    &#039;has_archive&#039; =&gt; true,
    &#039;supports&#039; =&gt; array(&#039;title&#039;, &#039;editor&#039;, &#039;author&#039;),
    &#039;rewrite&#039; =&gt; array(&#039;slug&#039; =&gt; &#039;member-info&#039;),
  );
  register_post_type(&#039;member_info&#039;, $args);
}
add_action(&#039;init&#039;, &#039;register_member_info_post_type&#039;);

function ks_create_member_info_post($user_id) {
  $user = get_userdata($user_id);
  $display_name = $user-&gt;display_name;
  $post_title = $display_name;

  $post_content = &#039;

  &lt;h3 class=&quot;wp-block-heading&quot;&gt;&lt;strong&gt;經歷&lt;/strong&gt;（公開資訊）&lt;/h3&gt;

&lt;p&gt;公開資訊&lt;/p&gt;

  &lt;h3 class=&quot;wp-block-heading&quot;&gt;&lt;strong&gt;專長&lt;/strong&gt;（公開資訊）&lt;/h3&gt;

&lt;p&gt;公開資訊&lt;/p&gt;
  &#039;;

  $post_data = array(
    &#039;post_title&#039; =&gt; $post_title,
    &#039;post_content&#039; =&gt; $post_content,
    &#039;post_status&#039; =&gt; &#039;publish&#039;,
    &#039;post_author&#039; =&gt; $user_id,
    &#039;post_type&#039; =&gt; &#039;member_info&#039;,
  );

  $post_id = wp_insert_post($post_data);

  if ($post_id) {
    update_field(&#039;member_content&#039;, &#039;登入會員限定內容&#039;, $post_id);
    update_field(&#039;member_current_position&#039;, &#039;目前任職&#039;, $post_id);
    update_field(&#039;member_experience&#039;, &#039;（公開資訊區）&lt;br&gt;信箱、網站、學歷、學號？&#039;, $post_id);
  }
}
add_action(&#039;user_register&#039;, &#039;ks_create_member_info_post&#039;);</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-auto-create-cpt/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>利用 Shortcode 在 WordPress 頁面顯示指定 Post Type 文章教學</title>
		<link>https://piglife.tw/technical-notes/wordpress-shortcode-post-type/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-shortcode-post-type/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 01 Nov 2017 17:24:52 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[functions.php]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[post type]]></category>
		<category><![CDATA[shortcode]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[wp_query]]></category>
		<category><![CDATA[自訂文章類型]]></category>
		<guid isPermaLink="false">https://piglife.tw/?p=265</guid>

					<description><![CDATA[介紹如何在 WordPress 中利用 shortcode 包裝 WP_Query 查詢指定文章類型...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在使用 WordPress 建置網站時，若頁面內容多以 shortcode 方式組合，想要在頁面中靈活顯示特定文章類型（Post Type）的文章，就需要將查詢文章的功能包裝成 shortcode。這樣可以方便在任何頁面或文章中插入，提升內容管理彈性。本文適合有基礎 PHP 與 WordPress 開發經驗的工程師或自學者。</p>
<h2 class="wp-block-heading">建立自訂 Shortcode 函式</h2>
<p>我們可以在主題的 <code>functions.php</code> 檔案中，撰寫一個函式來查詢指定的文章類型，並輸出 HTML 結構。以下為範例關鍵程式碼說明：</p>
<pre><code class="lang-php language-php php">function home_post_listing_shortcode( $atts ) {
    ob_start();
    // 建立 WP_Query 物件，查詢 post_type 為 &#039;post&#039; 的文章，限制顯示 3 篇，依標題排序（降冪）
    $query = new WP_Query( array(
        &#039;post_type&#039;      =&gt; &#039;post&#039;, // 可替換成想抓取的自訂文章類型
        &#039;posts_per_page&#039; =&gt; 3,      // 取出文章數量
        &#039;order&#039;          =&gt; &#039;DESC&#039;, // 排序方式 ASC(小-&gt;大), DESC(大-&gt;小)
        &#039;orderby&#039;        =&gt; &#039;title&#039; // 依標題排序
    ) );

    if ( $query-&gt;have_posts() ) {
        echo &#039;&lt;ul class=&quot;info-listing&quot;&gt;&#039;;
        while ( $query-&gt;have_posts() ) {
            $query-&gt;the_post();
            ?&gt;
            &lt;li id=&quot;post-&lt;?php the_ID(); ?&gt;&quot; &lt;?php post_class(); ?&gt;&gt;
                &lt;div class=&quot;category_name&quot;&gt;&lt;?php the_category(); ?&gt;&lt;/div&gt;
                &lt;?php the_date(&#039;Y-m-d at g:ia&#039;, &#039;&lt;div class=&quot;news_date&quot;&gt;&#039;, &#039;&lt;/div&gt;&#039;); ?&gt;
                &lt;a href=&quot;&lt;?php the_permalink(); ?&gt;&quot;&gt;&lt;?php the_title(); ?&gt;&lt;/a&gt;
            &lt;/li&gt;
            &lt;?php
        }
        echo &#039;&lt;/ul&gt;&#039;;
        wp_reset_postdata();
    }

    return ob_get_clean();
}</code></pre>
<h2 class="wp-block-heading">註冊 Shortcode</h2>
<p>將上述函式註冊為 shortcode，讓 WordPress 辨識並可在頁面中使用：</p>
<pre><code class="lang-php language-php php">add_shortcode( &#039;list-posts-home&#039;, &#039;home_post_listing_shortcode&#039; );</code></pre>
<p>註冊後，只要在 WordPress 後台的頁面編輯器中插入 <code>[list-posts-home]</code>，就會顯示該 shortcode 所輸出的文章列表。</p>
<h2 class="wp-block-heading">實際應用與延伸</h2>
<ul>
<li>可修改 <code>post_type</code> 參數為自訂文章類型，如 <code>product</code>、<code>event</code> 等。</li>
<li>可加入 <code>category_name</code> 或 <code>tax_query</code> 參數，篩選特定分類文章。</li>
<li>調整 <code>posts_per_page</code> 與排序條件，符合不同需求。</li>
<li>搭配頁面建構器（如 Visual Composer）使用，提升內容排版彈性。</li>
</ul>
<h2 class="wp-block-heading">常見坑點</h2>
<ul>
<li><code>order</code> 參數應為 <code>DESC</code>（大到小），原範例中寫成 <code>DSC</code> 會無效。</li>
<li>使用 <code>the_date()</code> 時，若多篇文章同一天只會顯示一次日期，改用 <code>the_time()</code> 可避免此問題。</li>
<li>記得使用 <code>wp_reset_postdata()</code> 恢復全域文章資料，避免影響其他查詢。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-text language-text text">function home_post_listing_shortcode( $atts ) {
    ob_start();
    $query = new WP_Query( array(
        &#039;post_type&#039;      =&gt; &#039;post&#039;,
        &#039;posts_per_page&#039; =&gt; 3,
        &#039;order&#039;          =&gt; &#039;DESC&#039;,
        &#039;orderby&#039;        =&gt; &#039;title&#039;,
    ) );

    if ( $query-&gt;have_posts() ) {
        echo &#039;&lt;ul class=&quot;info-listing&quot;&gt;&#039;;
        while ( $query-&gt;have_posts() ) {
            $query-&gt;the_post();
            ?&gt;
            &lt;li id=&quot;post-&lt;?php the_ID(); ?&gt;&quot; &lt;?php post_class(); ?&gt;&gt;
                &lt;div class=&quot;category_name&quot;&gt;&lt;?php the_category(); ?&gt;&lt;/div&gt;
                &lt;?php the_date(&#039;Y-m-d at g:ia&#039;, &#039;&lt;div class=&quot;news_date&quot;&gt;&#039;, &#039;&lt;/div&gt;&#039;); ?&gt;
                &lt;a href=&quot;&lt;?php the_permalink(); ?&gt;&quot;&gt;&lt;?php the_title(); ?&gt;&lt;/a&gt;
            &lt;/li&gt;
            &lt;?php
        }
        echo &#039;&lt;/ul&gt;&#039;;
        wp_reset_postdata();
    }

    return ob_get_clean();
}

add_shortcode( &#039;list-posts-home&#039;, &#039;home_post_listing_shortcode&#039; );</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-shortcode-post-type/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
