<?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>acf &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/acf/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Thu, 25 Dec 2025 05:53:50 +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>acf &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>WordPress Polylang 多語翻譯批次複製工具實作解析</title>
		<link>https://piglife.tw/technical-notes/wordpress-polylang-bulk-copy/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-polylang-bulk-copy/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 24 Dec 2025 22:21:27 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[acf]]></category>
		<category><![CDATA[Polylang]]></category>
		<category><![CDATA[taxonomy]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[多語翻譯]]></category>
		<category><![CDATA[批次複製]]></category>
		<category><![CDATA[特色圖片]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-polylang-bulk-copy/</guid>

					<description><![CDATA[介紹一款基於 Polylang 官方 API 的 WordPress 多語翻譯批次複製工具，支援任意...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在多語網站開發中，使用 Polylang 外掛管理翻譯文章是一種常見做法。但當需要將大量文章從某一語言批次複製到另一語言時，手動操作效率低且容易出錯。這段程式碼提供一個獨立於特定文章類型的批次翻譯複製工具，適合有基礎 WordPress 與 Polylang 使用經驗的工程師或自學者，幫助快速複製文章並保持翻譯關聯。</p>
<h2 class="wp-block-heading">工具功能與設計架構</h2>
<h3 class="wp-block-heading">1. 固定來源語言與多目標語言選擇</h3>
<p>程式碼中定義常數 <code>IR_SOURCE_LANG</code> 為來源語言（此處固定為中文 zh），目標語言則可從下拉選單選擇（預設有英文 en、日文 ja），方便擴充其他語言。</p>
<h3 class="wp-block-heading">2. 獨立後台介面</h3>
<p>透過 <code>add_submenu_page</code> 將工具掛載於 WordPress 後台「工具」選單下，並提供兩種操作模式：</p>
<ul>
<li>單筆測試：指定文章 ID，快速測試複製功能。</li>
<li>批次複製：依選定文章類型，批次複製該語言下所有文章。</li>
</ul>
<h3 class="wp-block-heading">3. 使用 Polylang 官方 API 確保語言設定與翻譯關聯</h3>
<p>核心複製流程依序使用 <code>wp_insert_post</code> 建立新文章，<code>pll_set_post_language</code> 設定語言，最後用 <code>pll_save_post_translations</code> 儲存翻譯群組關聯，確保與 Polylang 外掛的正確整合。</p>
<h2 class="wp-block-heading">核心複製流程解析</h2>
<h3 class="wp-block-heading">Step 1: 確認來源文章與語言</h3>
<p>先取得來源文章，確認文章類型與來源語言是否符合設定，避免誤複製。</p>
<pre><code class="lang-php language-php php">ir_set_lang_official( $source_id, IR_SOURCE_LANG );</code></pre>
<p>這行確保來源文章語言正確。</p>
<h3 class="wp-block-heading">Step 2: 建立新文章</h3>
<p>使用 <code>wp_insert_post</code> 複製文章標題、內容、狀態、作者、日期等基本欄位，確保新文章與原文一致。</p>
<h3 class="wp-block-heading">Step 3: 設定新文章語言</h3>
<p>新文章建立後，設定為目標語言，保持語言一致性。</p>
<h3 class="wp-block-heading">Step 4: 儲存翻譯關聯</h3>
<p>將新文章加入翻譯群組，讓 Polylang 知道這是原文的翻譯版本。</p>
<h3 class="wp-block-heading">Step 5: 複製分類法（Taxonomy）</h3>
<p>複製原文文章所屬的分類與標籤，但排除語言相關的 taxonomy，避免衝突。</p>
<h3 class="wp-block-heading">Step 6: 複製特色圖片</h3>
<p>如果原文有設定特色圖片，將同一張圖片設定給新文章，保持視覺一致性。</p>
<h3 class="wp-block-heading">Step 7: 複製 ACF 自訂欄位</h3>
<p>利用 Advanced Custom Fields (ACF) 官方 API 安全複製所有自訂欄位，避免欄位名稱變動造成錯誤。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li><strong>資料備份</strong>：批次操作前務必備份資料庫，避免誤操作造成資料遺失。</li>
<li><strong>語言擴充</strong>：可依需求擴充 <code>$allowed_target_langs</code> 陣列，支援更多語言。</li>
<li><strong>效能考量</strong>：批次複製大量文章時，可能造成伺服器負擔，可分批執行或加入排程。</li>
<li><strong>錯誤處理</strong>：目前以回傳日誌方式呈現，可擴充為錯誤通知或記錄檔。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>複製後的文章不會自動翻譯內容，僅複製原文內容，需後續人工或機器翻譯。</li>
<li>Polylang API 函式必須存在，否則功能無法使用。</li>
<li>ACF 複製需確保 ACF 外掛已啟用且函式存在。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code>&lt;?php
/**
 * Universal Polylang Bulk Copy Tool (Final Version - FIXED)
 * - 獨立在「工具 &rarr; Bulk Copy Translations」底下（不綁每個 post type）
 * - 任意 Post Type 下拉選單選擇
 * - 來源語言固定 zh
 * - 目標語言下拉選（en / ja，可自行擴充）
 * - 單筆測試 / 批次處理
 * - 已存在翻譯自動跳過
 * - Polylang 官方流程：wp_insert_post + pll_set_post_language + pll_save_post_translations
 * - taxonomy / featured image / ACF 全欄位複製（ACF 安全寫法）
 *

if ( ! defined( &#039;ABSPATH&#039; ) ) {
    exit;
}

define( &#039;IR_SOURCE_LANG&#039;, &#039;zh&#039; ); // 固定來源語言（Polylang code）

/* =========================
 * 共用：設定語言（官方 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 );
    }
}

/* =========================
 * 後台選單（獨立於 post type）
 * ========================= */
add_action( &#039;admin_menu&#039;, function () {
    add_submenu_page(
        &#039;tools.php&#039;,                         // 掛在「工具」
        &#039;Bulk Copy Translations (Official)&#039;, // 頁面標題
        &#039;Bulk Copy Translations&#039;,            // 左側選單名稱
        &#039;manage_options&#039;,                    // 權限
        &#039;ir-bulk-translations&#039;,              // slug
        &#039;ir_bulk_translations_page&#039;          // callback
    );
});

/* =========================
 * 後台頁面
 * ========================= */
function ir_bulk_translations_page() {

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

    // Polylang API 檢查
    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 不存在，請確認 Polylang 是否啟用。&lt;/p&gt;&lt;/div&gt;&#039;;
        return;
    }

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

    // 可選 post types（public）
    $post_types = get_post_types( [ &#039;public&#039; =&gt; true ], &#039;objects&#039; );

    // 預設值
    $current_post_type = isset( $_POST[&#039;ir_post_type&#039;] ) ? sanitize_text_field( wp_unslash( $_POST[&#039;ir_post_type&#039;] ) ) : &#039;post&#039;;
    if ( ! isset( $post_types[ $current_post_type ] ) ) {
        $current_post_type = &#039;post&#039;;
    }

    $current_target = isset( $_POST[&#039;ir_target_lang&#039;] ) ? sanitize_text_field( wp_unslash( $_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;通用翻譯複製工具（Polylang 官方流程）&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:#b32d2e"><strong>⚠️ 執行前請先備份資料庫</strong></p>';

    echo '

        .ir-box{background:#fff;border:1px solid #ccd0d4;padding:12px 14px;margin:12px 0;}
        .ir-row{margin:10px 0;}
        .ir-row label{display:inline-block;min-width:90px;font-weight:600;}
        .ir-pre{background:#fff;border:1px solid #ccd0d4;padding:10px;max-height:520px;overflow:auto;white-space:pre-wrap;}
    ';

    /* =========================
     * 單筆測試
     * ========================= */
    echo '<div class="ir-box">';
    echo '<div style="margin-top:0">單筆測試</div>';
    echo '';
    wp_nonce_field( 'ir_single' );

    echo '<div class="ir-row"><label>Post Type</label> ';
    echo '';
    foreach ( $post_types as $pt ) {
        printf(
            '%s (%s)',
            esc_attr( $pt-&gt;name ),
            selected( $current_post_type, $pt-&gt;name, false ),
            esc_html( $pt-&gt;labels-&gt;singular_name ),
            esc_html( $pt-&gt;name )
        );
    }
    echo '</div>';

    echo '<div class="ir-row"><label>文章 ID</label> ';
    echo '<input type="number" name="ir_test_id" required></div>';

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

    submit_button( '測試複製', 'secondary', 'ir_single_run' );
    echo '';
    echo '</div>';

    /* =========================
     * 批次處理
     * ========================= */
    echo '<div class="ir-box">';
    echo '<div style="margin-top:0">批次複製（來源語言：' . esc_html( IR_SOURCE_LANG ) . '）</div>';
    echo '';
    wp_nonce_field( 'ir_bulk' );

    echo '<div class="ir-row"><label>Post Type</label> ';
    echo '';
    foreach ( $post_types as $pt ) {
        printf(
            '%s (%s)',
            esc_attr( $pt-&gt;name ),
            selected( $current_post_type, $pt-&gt;name, false ),
            esc_html( $pt-&gt;labels-&gt;singular_name ),
            esc_html( $pt-&gt;name )
        );
    }
    echo '</div>';

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

    submit_button( '執行批次複製', 'primary', 'ir_bulk_run' );
    echo '';
    echo '</div>';

    /* =========================
     * 執行
     * ========================= */
    if ( isset( $_POST['ir_single_run'] ) ) {
        check_admin_referer( 'ir_single' );

        $post_id   = isset( $_POST['ir_test_id'] ) ? absint( $_POST['ir_test_id'] ) : 0;
        $target    = $current_target;
        $post_type = $current_post_type;

        echo '
<div>單筆結果</div>';
        $log = ir_clone_post( $post_id, $target, $post_type );

        echo '<pre class="ir-pre">' . esc_html( implode( "\n", $log ) ) . '</pre>';
    }

    if ( isset( $_POST['ir_bulk_run'] ) ) {
        check_admin_referer( 'ir_bulk' );

        $target    = $current_target;
        $post_type = $current_post_type;

        echo '
<div>批次結果（Post Type：' . esc_html( $post_type ) . '，目標語言：' . esc_html( strtoupper( $target ) ) . '）</div>';

        $posts = get_posts( [
            'post_type'      =&gt; $post_type,
            'posts_per_page' =&gt; -1,
            'post_status'    =&gt; 'any',
            'lang'           =&gt; IR_SOURCE_LANG, // Polylang 的語言 query var
            'fields'         =&gt; 'ids',
        ] );

        if ( empty( $posts ) ) {
            echo '
<p>找不到來源語言（' . esc_html( IR_SOURCE_LANG ) . '）的文章。</p>';
        } else {
            $output = [];
            foreach ( $posts as $id ) {
                $output = array_merge( $output, ir_clone_post( $id, $target, $post_type ) );
            }
            echo '<pre class="ir-pre">' . esc_html( implode( "\n", $output ) ) . '</pre>';
        }
    }

}

/* =========================
 * 核心：複製一篇文章 → 目標語言翻譯
 * ========================= */
function ir_clone_post( $source_id, $target_lang, $post_type ) {

    $log = [];

    if ( ! $source_id ) {
        return [ '❌ 文章 ID 不可為 0' ];
    }

    $src = get_post( $source_id );
    if ( ! $src ) {
        return [ "❌ 找不到文章 {$source_id}" ];
    }

    // 確保來源文章屬於選定的 post type（避免拿錯）
    if ( $src-&gt;post_type !== $post_type ) {
        return [ "❌ 文章 {$source_id} 的 post_type 是 {$src-&gt;post_type}，不是你選的 {$post_type}，已中止。" ];
    }

    // Step 1) 確保原文語言
    ir_set_lang_official( $source_id, IR_SOURCE_LANG );
    $log[] = "Step 1) 設定原文語言：{$source_id} → " . IR_SOURCE_LANG;

    // 取得翻譯群組
    $translations = function_exists( 'pll_get_post_translations' )
        ? pll_get_post_translations( $source_id )
        : [];

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

    // 已存在目標語言 → 跳過
    if ( ! empty( $translations[ $target_lang ] ) ) {
        $existing = (int) $translations[ $target_lang ];
        $log[] = "⚠️ 已存在 {$target_lang} 翻譯（ID {$existing}），跳過。";

        // debug 狀態
        if ( function_exists( 'pll_get_post_language' ) ) {
            $lang_debug = [];
            foreach ( $translations as $code =&gt; $pid ) {
                $lang_debug[] = $code . ':' . $pid . '(' . pll_get_post_language( $pid ) . ')';
            }
            $log[] = '🧪 目前語言狀態：' . implode( ', ', $lang_debug );
        }

        return $log;
    }

    // Step 2) 建立新文章
    $new_id = wp_insert_post( [
        'post_type'      =&gt; $post_type,
        'post_status'    =&gt; $src-&gt;post_status,
        'post_title'     =&gt; $src-&gt;post_title,
        'post_content'   =&gt; $src-&gt;post_content,
        'post_excerpt'   =&gt; $src-&gt;post_excerpt,
        'post_author'    =&gt; $src-&gt;post_author,
        'post_date'      =&gt; $src-&gt;post_date,
        'post_date_gmt'  =&gt; $src-&gt;post_date_gmt,
        'menu_order'     =&gt; $src-&gt;menu_order,
        'post_parent'    =&gt; $src-&gt;post_parent,
    ], true );

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

    // Step 3) 設定新文章語言
    ir_set_lang_official( $new_id, $target_lang );
    $log[] = "Step 3) 設定新文章語言：{$new_id} → {$target_lang}";

    // Step 4) 儲存翻譯關聯
    $translations[ $target_lang ] = $new_id;
    pll_save_post_translations( $translations );
    $log[] = 'Step 4) 儲存翻譯關聯：' . wp_json_encode( $translations );

    // Step 5) 複製 taxonomy（排除語言 taxonomy）
    $taxes = get_object_taxonomies( $post_type );
    $taxes = array_diff( $taxes, [ 'language', 'pll_language' ] );

    foreach ( $taxes as $tax ) {
        $terms = wp_get_object_terms( $source_id, $tax, [ 'fields' =&gt; 'ids' ] );
        if ( ! is_wp_error( $terms ) ) {
            wp_set_object_terms( $new_id, $terms, $tax, false );
        }
    }
    $log[] = 'Step 5) Taxonomy 複製完成';

    // Step 6) 複製特色圖片
    $thumb = get_post_thumbnail_id( $source_id );
    if ( $thumb ) {
        set_post_thumbnail( $new_id, $thumb );
        $log[] = 'Step 6) Featured Image 複製完成';
    } else {
        $log[] = 'Step 6) 原文沒有 Featured Image';
    }

    // Step 7) 複製 ACF 全欄位（官方安全寫法）
    if ( function_exists( 'get_field_objects' ) &amp;&amp; function_exists( 'update_field' ) ) {
        $fields = get_field_objects( $source_id );
        $count  = 0;

        if ( $fields ) {
            foreach ( $fields as $field ) {
                // 用 field key 複製最穩（不怕改欄位 name）
                update_field( $field['key'], $field['value'], $new_id );
                $count++;
            }
        }

        $log[] = "Step 7) ACF 全欄位複製完成（{$count} 個欄位）";
    } else {
        $log[] = 'Step 7) ACF API 不存在（get_field_objects / update_field），跳過 ACF 複製';
    }

    // 最後：語言檢查
    if ( function_exists( 'pll_get_post_language' ) ) {
        $src_lang_real = pll_get_post_language( $source_id );
        $new_lang_real = pll_get_post_language( $new_id );
        $log[] = "🧪 實際語言檢查：原文 {$source_id} 語言：{$src_lang_real}；新文 {$new_id} 語言：{$new_lang_real}";
    }

    $log[] = "✅ 完成：原文 {$source_id} → {$target_lang} 翻譯 {$new_id}";
    return $log;
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-polylang-bulk-copy/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 結合 MemberPress 與 ACF 自動更新文章存取權限狀態</title>
		<link>https://piglife.tw/technical-notes/wordpress-memberpress-acf-access-status/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-memberpress-acf-access-status/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 17 Dec 2025 22:20:39 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[acf]]></category>
		<category><![CDATA[MemberPress]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-memberpress-acf-access-status/</guid>

					<description><![CDATA[介紹如何在 WordPress 結合 MemberPress 與 ACF，自動更新文章的存取權限狀態...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在使用 WordPress 搭配會員管理外掛 MemberPress 時，常見需求是判斷文章是否有設定存取權限，並將此狀態同步到自訂欄位（ACF）中，方便前端或查詢邏輯使用。這段程式碼示範如何在文章儲存時自動檢查 MemberPress 的存取規則，並更新 ACF 的 True/False 欄位，適合熟悉 WordPress 開發並使用 MemberPress 與 ACF 的工程師。</p>
<h2 class="wp-block-heading">文章儲存時自動同步存取權限</h2>
<p>透過 <code>save_post</code> 鉤子，在文章儲存或更新時觸發檢查。程式碼會先排除自動儲存與修訂版本，避免重複執行。接著確認文章類型是否為支援的 <code>post</code> 或 <code>page</code>，並檢查 MemberPress 與 ACF 是否存在。</p>
<h3 class="wp-block-heading">重要程式碼片段</h3>
<pre><code class="lang-php language-php php">if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
    return;
}

$post_type = get_post_type($post_id);
if (!in_array($post_type, array(&#039;post&#039;, &#039;page&#039;))) {
    return;
}

if (!class_exists(&#039;MeprRule&#039;) || !function_exists(&#039;update_field&#039;)) {
    return;
}</code></pre>
<p>接著使用 <code>MeprRule::get_rules($post)</code> 取得該文章的 MemberPress 存取規則，判斷是否有規則存在，並更新 ACF 的 <code>ks_post_access</code> 欄位。</p>
<pre><code class="lang-php language-php php">$rules = MeprRule::get_rules($post);
$has_rule = !empty($rules);

update_field(&#039;ks_post_access&#039;, $has_rule, $post_id);</code></pre>
<h2 class="wp-block-heading">當 MemberPress 規則變更時批次更新所有文章</h2>
<p>除了單篇文章儲存時更新，也會在 MemberPress 規則新增、修改或刪除時觸發批次更新所有文章的存取狀態，確保資料一致。</p>
<pre><code class="lang-php language-php php">function ks_update_all_posts_access_status() {
    if (!function_exists(&#039;update_field&#039;)) {
        return;
    }
    $posts = get_posts(array(
        &#039;post_type&#039; =&gt; &#039;post&#039;,
        &#039;post_status&#039; =&gt; &#039;publish&#039;,
        &#039;numberposts&#039; =&gt; -1,
        &#039;fields&#039; =&gt; &#039;ids&#039;
    ));
    foreach ($posts as $post_id) {
        $post = get_post($post_id);
        $rules = MeprRule::get_rules($post);
        $has_rule = !empty($rules);
        update_field(&#039;ks_post_access&#039;, $has_rule, $post_id);
    }
}</code></pre>
<h2 class="wp-block-heading">後台介面批次更新功能</h2>
<p>為方便管理，新增 WordPress 後台子選單，讓管理者可以手動觸發更新所有文章的存取權限狀態，並提供操作說明。</p>
<pre><code class="lang-php language-php php">function ks_add_bulk_update_access_status() {
    add_submenu_page(
        &#039;edit.php&#039;,
        &#039;Update Access Status&#039;,
        &#039;Update Access Status&#039;,
        &#039;manage_options&#039;,
        &#039;ks-bulk-update-access-status&#039;,
        &#039;ks_bulk_update_access_status_page&#039;
    );
}
add_action(&#039;admin_menu&#039;, &#039;ks_add_bulk_update_access_status&#039;);</code></pre>
<p>介面中包含按鈕與確認提示，確保操作安全。</p>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<ul>
<li>透過 ACF 的 <code>ks_post_access</code> 欄位，前端查詢可以輕鬆過濾有存取限制的文章。</li>
<li>可結合 GreenShift Query Loop 或其他查詢工具，提升前端顯示效率。</li>
<li>若會員規則較複雜，建議優化批次更新邏輯，避免效能瓶頸。</li>
<li>可加入快取機制或非同步處理，減少儲存時的阻塞。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>請確保已建立名為 <code>ks_post_access</code> 的 True/False ACF 欄位，且欄位名稱與程式碼一致。</li>
<li>若 MemberPress 或 ACF 外掛未啟用，程式會自動跳過，避免錯誤。</li>
<li>批次更新文章時，若文章數量龐大，可能會耗時較久，建議在低流量時段執行。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">
&lt;?php
// 在文章儲存/更新時自動檢查並更新 ACF 欄位
function ks_update_mepr_access_status($post_id) {
    // 避免自動儲存時執行
    if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
        return;
    }

    // 檢查是否為支援的文章類型
    $post_type = get_post_type($post_id);
    if (!in_array($post_type, array(&#039;post&#039;, &#039;page&#039;))) {
        return;
    }

    // 檢查是否有 MemberPress 和 ACF
    if (!class_exists(&#039;MeprRule&#039;) || !function_exists(&#039;update_field&#039;)) {
        return;
    }

    $post = get_post($post_id);
    if (!$post) {
        return;
    }

    // 檢查該文章是否有 MemberPress 規則
    $rules = MeprRule::get_rules($post);
    $has_rule = !empty($rules);

    // 更新 ACF 欄位
    update_field(&#039;ks_post_access&#039;, $has_rule, $post_id);

    // 可選：記錄 log 用於除錯
    error_log(&quot;KS Post ID: $post_id - Access Rule Status: &quot; . ($has_rule ? &#039;Yes&#039; : &#039;No&#039;));
}

// 掛載到文章儲存的 hook
add_action(&#039;save_post&#039;, &#039;ks_update_mepr_access_status&#039;, 20, 1);

// 也可以掛載到 MemberPress 規則更新時
add_action(&#039;mepr-rule-saved&#039;, &#039;ks_update_all_posts_access_status&#039;);
add_action(&#039;mepr-rule-deleted&#039;, &#039;ks_update_all_posts_access_status&#039;);

// 當 MemberPress 規則變更時，更新所有相關文章
function ks_update_all_posts_access_status() {
    if (!function_exists(&#039;update_field&#039;)) {
        return;
    }

    $posts = get_posts(array(
        &#039;post_type&#039; =&gt; &#039;post&#039;,
        &#039;post_status&#039; =&gt; &#039;publish&#039;,
        &#039;numberposts&#039; =&gt; -1,
        &#039;fields&#039; =&gt; &#039;ids&#039;
    ));

    foreach ($posts as $post_id) {
        $post = get_post($post_id);
        $rules = MeprRule::get_rules($post);
        $has_rule = !empty($rules);

        update_field(&#039;ks_post_access&#039;, $has_rule, $post_id);
    }
}

// 管理後台增加批量更新功能
function ks_add_bulk_update_access_status() {
    add_submenu_page(
        &#039;edit.php&#039;,
        &#039;Update Access Status&#039;,
        &#039;Update Access Status&#039;,
        &#039;manage_options&#039;,
        &#039;ks-bulk-update-access-status&#039;,
        &#039;ks_bulk_update_access_status_page&#039;
    );
}
add_action(&#039;admin_menu&#039;, &#039;ks_add_bulk_update_access_status&#039;);

function ks_bulk_update_access_status_page() {
    if (isset($_POST[&#039;ks_update_all&#039;])) {
        ks_update_all_posts_access_status();
        echo &#039;&lt;div class=&quot;notice notice-success&quot;&gt;&lt;p&gt;所有文章的存取權限狀態已更新完成！&lt;/p&gt;&lt;/div&gt;&#039;;
    }

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

&lt;h1&gt;KS 更新文章存取權限狀態&lt;/h1&gt;

&lt;p&gt;此功能會檢查所有文章的 MemberPress 存取權限設定，並更新對應的 ACF 欄位。&lt;/p&gt;

        &lt;form method=&quot;post&quot;&gt;
            &lt;input type=&quot;submit&quot; name=&quot;ks_update_all&quot; class=&quot;button-primary&quot; value=&quot;更新所有文章狀態&quot; 
                   onclick=&quot;return confirm(&#039;確定要更新所有文章的存取權限狀態嗎？&#039;);&quot;&gt;
        &lt;/form&gt;
    &lt;/div&gt;
    &lt;?php
}</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-memberpress-acf-access-status/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 PHP 與 DOMDocument 自動產生 WordPress 文章預覽內容</title>
		<link>https://piglife.tw/technical-notes/php-domdocument-wordpress-preview/</link>
					<comments>https://piglife.tw/technical-notes/php-domdocument-wordpress-preview/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Fri, 12 Dec 2025 22:20:47 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[acf]]></category>
		<category><![CDATA[DOMDocument]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/php-domdocument-wordpress-preview/</guid>

					<description><![CDATA[介紹如何使用 PHP 的 DOMDocument 在 WordPress 中自動擷取文章內容第一個區...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 開發中，有時候需要自動從文章內容擷取一部分作為預覽，方便在前台或後台顯示摘要。這段程式碼示範如何利用 PHP 的 DOMDocument 解析文章 HTML，並將第一個區塊內容存入自訂欄位，適合希望自動管理文章預覽的開發者或自學者。</p>
<h2 class="wp-block-heading">主要功能說明</h2>
<p>這段程式碼的核心目標是：</p>
<ul>
<li>避免在自動儲存或文章修訂版本時重複執行</li>
<li>只處理文章(post)類型</li>
<li>取得文章內容並套用 WordPress 內建的內容過濾器（the_content）</li>
<li>使用 DOMDocument 將 HTML 內容解析為節點樹</li>
<li>擷取第一個 HTML 區塊（元素節點）作為預覽內容</li>
<li>將擷取結果寫入 Advanced Custom Fields (ACF) 的自訂欄位</li>
</ul>
<p>這樣的設計能確保預覽內容是有效的 HTML 片段，且不會因為純文字截斷而破壞標籤結構。</p>
<h2 class="wp-block-heading">程式碼解析</h2>
<h3 class="wp-block-heading">避免不必要的觸發</h3>
<pre><code class="lang-php language-php php">if (defined(&#039;DOING_AUTOSAVE&#039;) &amp;&amp; DOING_AUTOSAVE) return;
if (wp_is_post_revision($post_id)) return;</code></pre>
<p>這兩個判斷用來避免在 WordPress 自動儲存或文章修訂版本時執行預覽生成，減少不必要的資源消耗。</p>
<h3 class="wp-block-heading">文章類型與內容驗證</h3>
<pre><code class="lang-php language-php php">$post = get_post($post_id);
if ($post-&gt;post_type !== &#039;post&#039;) return;
$content = $post-&gt;post_content;
if (empty($content)) return;</code></pre>
<p>確保只處理標準文章(post)，且內容不為空。</p>
<h3 class="wp-block-heading">套用內容過濾器</h3>
<pre><code class="lang-php language-php php">$filtered_content = apply_filters(&#039;the_content&#039;, $content);</code></pre>
<p>這步驟會讓內容經過 WordPress 內建的過濾器處理，例如短碼解析、自動換行等，確保後續解析的 HTML 是完整且符合前台顯示的狀態。</p>
<h3 class="wp-block-heading">使用 DOMDocument 解析 HTML</h3>
<pre><code class="lang-php language-php php">libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc-&gt;loadHTML(&#039;&lt;?xml encoding=&quot;utf-8&quot; ?&gt;&#039; . $filtered_content);
libxml_clear_errors();</code></pre>
<p>因為 HTML 可能不完全符合 XML 規範，先抑制 libxml 錯誤，並在字串前加上 XML 編碼宣告，確保 UTF-8 正確處理。</p>
<h3 class="wp-block-heading">擷取第一個區塊元素</h3>
<pre><code class="lang-php language-php php">$body = $doc-&gt;getElementsByTagName(&#039;body&#039;)-&gt;item(0);
$output_blocks = [];
$count = 0;
$max_blocks = 1;

foreach ($body-&gt;childNodes as $node) {
    if ($node-&gt;nodeType === XML_ELEMENT_NODE) {
        $output_blocks[] = $doc-&gt;saveHTML($node);
        $count++;
        if ($count &gt;= $max_blocks) break;
    }
}

$preview_html = implode(&quot;\n&quot;, $output_blocks);</code></pre>
<p>這段程式碼從 body 標籤下取得第一個元素節點（例如第一個段落或區塊標籤），並將其轉成 HTML 字串。這樣做的好處是避免截取不完整的 HTML，保持標籤結構完整。</p>
<h3 class="wp-block-heading">寫入自訂欄位</h3>
<pre><code class="lang-php language-php php">update_field(&#039;ks_post_preview&#039;, $preview_html, $post_id);</code></pre>
<p>利用 ACF 提供的 update_field 函式，將預覽內容寫入名為 ks_post_preview 的自訂欄位，方便後續在前台或後台調用。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li>可依需求調整 $max_blocks 參數，擷取多個區塊作為預覽</li>
<li>若文章內容包含複雜結構，DOMDocument 解析可確保 HTML 不會因截斷而破損</li>
<li>可結合 WordPress 的鉤子（如 save_post）自動更新預覽，減少手動維護成本</li>
<li>注意 ACF 欄位名稱需與後台設定一致，否則無法成功更新</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>DOMDocument 解析 HTML 時可能因不標準標籤產生警告，使用 libxml_use_internal_errors 可避免影響</li>
<li>若文章內容過短或無有效區塊，預覽欄位可能為空，需在前端做好容錯處理</li>
<li>自動儲存與修訂版本判斷是避免重複觸發的關鍵，缺少可能導致效能問題</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
function ks_generate_post_preview($post_id) {
    // 避免自動儲存時觸發
    if (defined(&#039;DOING_AUTOSAVE&#039;) &amp;&amp; DOING_AUTOSAVE) return;
    if (wp_is_post_revision($post_id)) return;

    $post = get_post($post_id);
    if ($post-&gt;post_type !== &#039;post&#039;) return;

    $content = $post-&gt;post_content;
    if (empty($content)) return;

    // 套用 the_content 濾鏡
    $filtered_content = apply_filters(&#039;the_content&#039;, $content);

    // 使用 DOMDocument 分析 HTML
    libxml_use_internal_errors(true);
    $doc = new DOMDocument();
    $doc-&gt;loadHTML(&#039;&lt;?xml encoding=&quot;utf-8&quot; ?&gt;&#039; . $filtered_content);
    libxml_clear_errors();

    $body = $doc-&gt;getElementsByTagName(&#039;body&#039;)-&gt;item(0);
    $output_blocks = [];
    $count = 0;
    $max_blocks = 1;

    foreach ($body-&gt;childNodes as $node) {
        if ($node-&gt;nodeType === XML_ELEMENT_NODE) {
            $output_blocks[] = $doc-&gt;saveHTML($node);
            $count++;
            if ($count &gt;= $max_blocks) break;
        }
    }

    $preview_html = implode(&quot;\n&quot;, $output_blocks);

    // 寫入 ACF 欄位
    update_field(&#039;ks_post_preview&#039;, $preview_html, $post_id);
}
add_action(&#039;save_post&#039;, &#039;ks_generate_post_preview&#039;);</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/php-domdocument-wordpress-preview/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>
	</channel>
</rss>
