<?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>PHP &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/php/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Fri, 02 Jan 2026 22:20:44 +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>PHP &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>使用 Firebase Firestore 匯出抽獎資料為 CSV 檔案的實作說明</title>
		<link>https://piglife.tw/technical-notes/firebase-firestore-csv-export/</link>
					<comments>https://piglife.tw/technical-notes/firebase-firestore-csv-export/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Fri, 02 Jan 2026 22:20:44 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[CSV匯出]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[Firestore]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[前端匯出]]></category>
		<category><![CDATA[資料分頁]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/firebase-firestore-csv-export/</guid>

					<description><![CDATA[本文說明如何結合 Firebase Firestore 與前端 JavaScript 實作抽獎資料的...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>本篇文章介紹如何在 PHP 環境中結合 Firebase Firestore，實作一個前端按鈕匯出抽獎資料為 CSV 檔案的功能。此功能適合需要將 Firestore 資料批次匯出，並提供給非技術人員下載的場景。讀者需具備基本 PHP 與 JavaScript 知識，並了解 Firebase Firestore 的基本操作。</p>
<h2 class="wp-block-heading">Firebase 初始化與匿名登入</h2>
<p>在前端使用 Firebase SDK，首先透過 <code>initializeApp</code> 初始化 Firebase，並使用匿名登入 (<code>signInAnonymously</code>) 取得存取 Firestore 的權限。這樣做可以避免在伺服器端直接操作 Firebase，減少安全風險。</p>
<pre><code class="lang-js language-js js">const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
await signInAnonymously(auth);
const db = getFirestore(app);</code></pre>
<h2 class="wp-block-heading">分頁抓取 Firestore 資料</h2>
<p>Firestore 資料量可能很大，因此使用分頁查詢避免一次讀取過多資料造成效能問題。透過 <code>orderBy(&quot;__name__&quot;)</code> 與 <code>startAfter</code> 搭配 <code>limit</code>，逐頁取得資料直到抓完為止。</p>
<pre><code class="lang-js language-js js">async function fetchAllEntriesPaged(db, pageSize = 500) {
  const colRef = collection(db, ENTRY_COLLECTION_PATH);
  let all = [];
  let lastDoc = null;
  while (true) {
    let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
    if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));
    const snap = await getDocs(q);
    if (snap.empty) break;
    snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
    lastDoc = snap.docs[snap.docs.length - 1];
    if (snap.size &lt; pageSize) break;
  }
  return all;
}</code></pre>
<h2 class="wp-block-heading">CSV 格式處理與下載</h2>
<p>匯出 CSV 需要將資料轉成符合格式的字串，並處理特殊字元（如逗號、雙引號、換行）。<code>csvEscape</code> 函式負責這部分。下載時利用 Blob 物件與 <code>URL.createObjectURL</code> 產生可下載的連結。</p>
<pre><code class="lang-js language-js js">function csvEscape(val) {
  if (val === null || val === undefined) return &quot;&quot;;
  const s = String(val);
  if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
  return s;
}

function downloadCSV(lines, filename) {
  const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;);
  const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
  const url = URL.createObjectURL(blob);
  const a = document.createElement(&quot;a&quot;);
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}</code></pre>
<h2 class="wp-block-heading">時間戳記格式轉換</h2>
<p>Firestore 的時間欄位格式特殊，可能是 Timestamp 物件或其他形式，透過 <code>tsToISO</code> 函式統一轉成 ISO 8601 字串，方便 CSV 讀取與後續處理。</p>
<pre><code class="lang-js language-js js">function tsToISO(v) {
  try {
    if (!v) return &quot;&quot;;
    if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
    if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
    return String(v);
  } catch {
    return &quot;&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實作匯出流程</h2>
<p>按下「下載 CSV」按鈕後，觸發 <code>runExport</code> 函式，依序完成初始化、登入、資料分頁抓取、CSV 組合、下載檔案與狀態更新。過程中會禁用按鈕避免重複點擊，並顯示目前狀態。</p>
<pre><code class="lang-js language-js js">async function runExport() {
  $status.textContent = &quot;&quot;;
  $btn.disabled = true;
  $btn.textContent = &quot;匯出中...&quot;;
  try {
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    await signInAnonymously(auth);
    const db = getFirestore(app);
    $status.textContent = &quot;正在抓取資料...&quot;;
    const rows = await fetchAllEntriesPaged(db, 500);
    if (!rows.length) {
      $status.textContent = &quot;沒有任何資料可匯出。&quot;;
      return;
    }
    const headers = [&quot;docId&quot;, &quot;name&quot;, &quot;classroom&quot;, &quot;teacher&quot;, &quot;entryDate&quot;, &quot;entryNumber&quot;, &quot;isWinner&quot;, &quot;drawTime&quot;, &quot;drawnBy&quot;, &quot;createdAt&quot;, &quot;updatedAt&quot;];
    const lines = [];
    lines.push(headers.map(csvEscape).join(&quot;,&quot;));
    for (const r of rows) {
      const line = [
        r.id ?? &quot;&quot;,
        r.name ?? &quot;&quot;,
        r.classroom ?? &quot;&quot;,
        r.teacher ?? &quot;&quot;,
        r.entryDate ?? &quot;&quot;,
        r.entryNumber ?? &quot;&quot;,
        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
        r.drawTime ?? &quot;&quot;,
        r.drawnBy ?? &quot;&quot;,
        tsToISO(r.createdAt),
        tsToISO(r.updatedAt),
      ].map(csvEscape).join(&quot;,&quot;);
      lines.push(line);
    }
    const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
    downloadCSV(lines, filename);
    $status.textContent = `匯出完成：共 ${rows.length} 筆`;
  } catch (e) {
    console.error(e);
    $status.textContent = `匯出失敗：${e.message}`;
  } finally {
    $btn.disabled = false;
    $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實際應用與延伸</h2>
<p>此實作適合用於管理後台快速匯出 Firestore 資料，方便非技術人員下載報表。未來可擴充支援更多資料欄位，或改用後端匯出以保護 Firebase 金鑰安全。也可以加入進度條或分頁匯出功能，提升使用體驗。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>Firebase 配置需正確填寫並允許匿名登入。</li>
<li>Firestore 權限規則需設定允許讀取資料。</li>
<li>大量資料匯出時，注意瀏覽器記憶體限制與執行時間。</li>
<li>CSV 格式需妥善處理特殊字元避免格式錯亂。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">
&lt;?php
function at_christmas_export_menu_page()
{
    ?&gt;
    &lt;div class=&quot;wrap&quot;&gt;

&lt;h1&gt;匯出抽獎資料 CSV&lt;/h1&gt;

&lt;p&gt;
            &lt;button id=&quot;at-export-btn&quot; class=&quot;button button-primary&quot;&gt;⬇️ 下載 CSV&lt;/button&gt;
            &lt;span id=&quot;at-export-status&quot; style=&quot;margin-left:10px;&quot;&gt;&lt;/span&gt;
        &lt;/p&gt;
    &lt;/div&gt;

    &lt;script type=&quot;module&quot;&gt;
        import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js&quot;;
        import { getAuth, signInAnonymously } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js&quot;;
        import {
            getFirestore,
            collection,
            query,
            getDocs,
            orderBy,
            limit,
            startAfter
        } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js&quot;;

        const firebaseConfig = {
            apiKey: &quot;YOUR_API_KEY&quot;,
            authDomain: &quot;YOUR_AUTH_DOMAIN&quot;,
            projectId: &quot;YOUR_PROJECT_ID&quot;,
            storageBucket: &quot;YOUR_STORAGE_BUCKET&quot;,
            messagingSenderId: &quot;YOUR_MESSAGING_SENDER_ID&quot;,
            appId: &quot;YOUR_APP_ID&quot;,
            measurementId: &quot;YOUR_MEASUREMENT_ID&quot;
        };

        const projectId = firebaseConfig.projectId || &quot;YOUR_PROJECT_ID&quot;;
        const ENTRY_COLLECTION_PATH = `artifacts/${projectId}/public/data/christmas_raffle_entries`;

        const $btn = document.getElementById(&quot;at-export-btn&quot;);
        const $status = document.getElementById(&quot;at-export-status&quot;);

        function getTodayDate() {
            const now = new Date();
            const y = now.getFullYear();
            const m = String(now.getMonth() + 1).padStart(2, &quot;0&quot;);
            const d = String(now.getDate()).padStart(2, &quot;0&quot;);
            return `${y}-${m}-${d}`;
        }

        function csvEscape(val) {
            if (val === null || val === undefined) return &quot;&quot;;
            const s = String(val);
            if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
            return s;
        }

        function tsToISO(v) {
            try {
                if (!v) return &quot;&quot;;
                if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
                if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
                return String(v);
            } catch {
                return &quot;&quot;;
            }
        }

        async function fetchAllEntriesPaged(db, pageSize = 500) {
            const colRef = collection(db, ENTRY_COLLECTION_PATH);

            let all = [];
            let lastDoc = null;

            while (true) {
                let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
                if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));

                const snap = await getDocs(q);
                if (snap.empty) break;

                snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
                lastDoc = snap.docs[snap.docs.length - 1];

                if (snap.size &lt; pageSize) break;
            }
            return all;
        }

        function downloadCSV(lines, filename) {
            const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;); // BOM for Excel
            const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
            const url = URL.createObjectURL(blob);
            const a = document.createElement(&quot;a&quot;);
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            a.remove();
            URL.revokeObjectURL(url);
        }

        async function runExport() {
            $status.textContent = &quot;&quot;;
            $btn.disabled = true;
            $btn.textContent = &quot;匯出中...&quot;;

            try {
                const app = initializeApp(firebaseConfig);
                const auth = getAuth(app);
                await signInAnonymously(auth);

                const db = getFirestore(app);

                $status.textContent = &quot;正在抓取資料...&quot;;
                const rows = await fetchAllEntriesPaged(db, 500);

                if (!rows.length) {
                    $status.textContent = &quot;沒有任何資料可匯出。&quot;;
                    return;
                }

                const headers = [
                    &quot;docId&quot;,
                    &quot;name&quot;,
                    &quot;classroom&quot;,
                    &quot;teacher&quot;,
                    &quot;entryDate&quot;,
                    &quot;entryNumber&quot;,
                    &quot;isWinner&quot;,
                    &quot;drawTime&quot;,
                    &quot;drawnBy&quot;,
                    &quot;createdAt&quot;,
                    &quot;updatedAt&quot;
                ];

                const lines = [];
                lines.push(headers.map(csvEscape).join(&quot;,&quot;));

                for (const r of rows) {
                    const line = [
                        r.id ?? &quot;&quot;,
                        r.name ?? &quot;&quot;,
                        r.classroom ?? &quot;&quot;,
                        r.teacher ?? &quot;&quot;,
                        r.entryDate ?? &quot;&quot;,
                        r.entryNumber ?? &quot;&quot;,
                        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
                        r.drawTime ?? &quot;&quot;,
                        r.drawnBy ?? &quot;&quot;,
                        tsToISO(r.createdAt),
                        tsToISO(r.updatedAt),
                    ].map(csvEscape).join(&quot;,&quot;);
                    lines.push(line);
                }

                const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
                downloadCSV(lines, filename);

                $status.textContent = `匯出完成：共 ${rows.length} 筆`;
            } catch (e) {
                console.error(e);
                $status.textContent = `匯出失敗：${e.message}`;
            } finally {
                $btn.disabled = false;
                $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
            }
        }

        $btn.addEventListener(&quot;click&quot;, runExport);
    &lt;/script&gt;
&lt;?php
}
?&gt;</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/firebase-firestore-csv-export/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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>WooCommerce 後台訂單自訂欄位資料儲存實作說明</title>
		<link>https://piglife.tw/technical-notes/woocommerce-order-meta-save/</link>
					<comments>https://piglife.tw/technical-notes/woocommerce-order-meta-save/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Sun, 28 Dec 2025 22:20:33 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[woocommerce]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[後台欄位儲存]]></category>
		<category><![CDATA[訂單自訂欄位]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/woocommerce-order-meta-save/</guid>

					<description><![CDATA[說明如何在 WooCommerce 後台訂單編輯頁面保存自訂欄位資料，透過安全且兼容新版訂單存儲的方...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WooCommerce 訂單後台管理時，常會需要額外新增自訂欄位以收集特定資訊，例如客戶的聯絡電話。這段程式碼示範如何在訂單編輯頁面保存自訂欄位資料，確保資料能正確存入訂單元資料中。適合已熟悉 WooCommerce 且想擴充後台訂單欄位功能的工程師或自學者。</p>
<h2 class="wp-block-heading">為什麼需要自訂欄位儲存機制</h2>
<p>WooCommerce 預設訂單資料結構無法涵蓋所有業務需求，因此經常會透過自訂欄位來擴充。這些欄位必須在後台訂單編輯時能夠被正確讀取與保存，否則資料會遺失。</p>
<h2 class="wp-block-heading">使用 woocommerce_process_shop_order_meta 鉤子</h2>
<p>這個 action 鉤子會在 WooCommerce 處理訂單後台編輯表單時觸發，適合用來攔截並保存自訂欄位資料。</p>
<pre><code class="lang-php language-php php">add_action(&#039;woocommerce_process_shop_order_meta&#039;, function ($order_id) {
    // 權限檢查，避免非授權使用者修改訂單
    if (!current_user_can(&#039;edit_shop_order&#039;, $order_id))
        return;

    $key = &#039;_shipping_phone&#039;; // 自訂欄位名稱

    // 確認表單有送出該欄位
    if (!isset($_POST[$key]))
        return;

    // 清理輸入資料，避免 XSS 或其他注入風險
    $value = wc_clean(wp_unslash($_POST[$key]));

    // 取得訂單物件
    $order = wc_get_order($order_id);
    if (!$order)
        return;

    // 使用 WC_Order API 更新訂單元資料
    $order-&gt;update_meta_data($key, $value);
    $order-&gt;save(); // 必須呼叫 save() 才會寫入資料庫
}, 50);</code></pre>
<h3 class="wp-block-heading">關鍵說明</h3>
<ul>
<li><code>current_user_can</code> 用來確保只有有編輯訂單權限的使用者能執行更新。</li>
<li><code>wc_clean</code> 和 <code>wp_unslash</code> 是 WordPress 與 WooCommerce 提供的安全函式，確保輸入資料安全。</li>
<li>使用 <code>update_meta_data</code> 與 <code>save</code> 是 WooCommerce 推薦的寫入方式，兼容新版 HPOS（高效訂單存儲系統）與舊版 postmeta。</li>
</ul>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<p>此方法可擴充至任何自訂欄位，只要對應修改 <code>$key</code> 與表單名稱即可。實務中，還可搭配後台欄位輸入介面（如使用 <code>woocommerce_admin_order_data_after_billing_address</code> 鉤子）來完整實作自訂欄位的讀寫。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>忘記呼叫 <code>$order-&gt;save()</code> 將導致資料無法寫入。</li>
<li>欄位名稱建議加底線開頭避免與 WooCommerce 原生欄位衝突。</li>
<li>權限檢查不可省略，避免安全問題。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
add_action(&#039;woocommerce_process_shop_order_meta&#039;, function ($order_id) {
    if (!current_user_can(&#039;edit_shop_order&#039;, $order_id))
        return;

    $key = &#039;_shipping_phone&#039;;

    if (!isset($_POST[$key]))
        return;

    $value = wc_clean(wp_unslash($_POST[$key]));

    $order = wc_get_order($order_id);
    if (!$order)
        return;

    $order-&gt;update_meta_data($key, $value);
    $order-&gt;save();
}, 50);</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/woocommerce-order-meta-save/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>WordPress 取得用戶留言過文章列表與留言數量的實作教學</title>
		<link>https://piglife.tw/technical-notes/wordpress-user-commented-posts/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-user-commented-posts/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Tue, 16 Dec 2025 22:21:39 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[留言系統]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-user-commented-posts/</guid>

					<description><![CDATA[介紹如何在 WordPress 中取得用戶留言過的文章列表及留言數量，並整合至會員後台，方便展示用戶...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>本篇文章介紹如何在 WordPress 中取得特定用戶曾留言過的文章列表，並且顯示該用戶在每篇文章的留言數量。這段程式碼適合用於會員系統或社群互動功能，方便開發者在會員後台展示用戶的互動紀錄。目標讀者為具備 PHP 和 WordPress 基礎，想了解如何結合資料庫查詢與文章篩選的工程師或自學者。</p>
<h2 class="wp-block-heading">取得用戶留言過的文章 ID</h2>
<p>透過 WordPress 的全域變數 <code>$wpdb</code>，使用 SQL 查詢 <code>comments</code> 資料表，取得指定用戶的留言所對應的文章 ID，並去除重複。此函式回傳整數陣列。</p>
<pre><code class="lang-php language-php php">function ks_get_user_commented_post_ids( $user_id ) {
    global $wpdb;

    $query = $wpdb-&gt;prepare(
        &quot;SELECT DISTINCT comment_post_ID FROM {$wpdb-&gt;comments} WHERE user_id = %d&quot;,
        $user_id
    );

    $results = $wpdb-&gt;get_col( $query );
    return array_map( &#039;intval&#039;, $results );
}</code></pre>
<h3 class="wp-block-heading">為什麼使用 SQL 查詢？</h3>
<p>直接查詢資料庫效率較高，避免使用 WordPress 內建函式多次查詢留言，尤其用戶留言量大時更具效能優勢。</p>
<h2 class="wp-block-heading">取得用戶留言過且仍有效的文章物件</h2>
<p>此函式會先取得用戶留言過的文章 ID，再逐一檢查文章狀態是否為「已發佈」且文章類型為「post」，最後使用 <code>get_posts</code> 取得完整文章物件陣列，並以發佈日期排序。</p>
<pre><code class="lang-php language-php php">function ks_get_user_commented_posts( $user_id = 0 ) {
    if ( ! $user_id ) {
        $user_id = get_current_user_id();
    }

    if ( ! $user_id ) {
        return array();
    }

    $commented_post_ids = ks_get_user_commented_post_ids( $user_id );

    if ( empty( $commented_post_ids ) ) {
        return array();
    }

    $valid_post_ids = array();
    foreach ($commented_post_ids as $post_id) {
        $post = get_post($post_id);
        if ($post &amp;&amp; $post-&gt;post_status === &#039;publish&#039; &amp;&amp; $post-&gt;post_type === &#039;post&#039;) {
            $valid_post_ids[] = $post_id;
        }
    }

    if (empty($valid_post_ids)) {
        return array();
    }

    $posts = get_posts( array(
        &#039;post_type&#039;      =&gt; &#039;post&#039;,
        &#039;post_status&#039;    =&gt; &#039;publish&#039;,
        &#039;posts_per_page&#039; =&gt; -1,
        &#039;post__in&#039;       =&gt; $valid_post_ids,
        &#039;orderby&#039;        =&gt; &#039;date&#039;,
        &#039;order&#039;          =&gt; &#039;DESC&#039;
    ) );

    return $posts;
}</code></pre>
<h3 class="wp-block-heading">為什麼要篩選文章狀態？</h3>
<p>確保只顯示目前可見的文章，避免用戶看到草稿或已刪除的文章，提升使用體驗與資料正確性。</p>
<h2 class="wp-block-heading">取得用戶在指定文章的留言數量</h2>
<p>同樣利用 <code>$wpdb</code> 執行 SQL 查詢，計算該用戶在特定文章的留言數，回傳整數。</p>
<pre><code class="lang-php language-php php">function ks_get_user_comment_count_for_post( $post_id, $user_id = 0 ) {
    if ( ! $user_id ) {
        $user_id = get_current_user_id();
    }

    if ( ! $user_id ) {
        return 0;
    }

    global $wpdb;

    $count = $wpdb-&gt;get_var( $wpdb-&gt;prepare(
        &quot;SELECT COUNT(*) FROM {$wpdb-&gt;comments} WHERE comment_post_ID = %d AND user_id = %d&quot;,
        $post_id,
        $user_id
    ) );

    return intval($count);
}</code></pre>
<h2 class="wp-block-heading">在 MemberPress 會員後台新增「我的留言」分頁</h2>
<p>透過 <code>mepr_account_nav</code> 與 <code>mepr_account_nav_content</code> 兩個鉤子，分別新增導覽選單與內容區塊，顯示用戶留言過的文章列表。</p>
<h3 class="wp-block-heading">導覽選單新增項目</h3>
<pre><code class="lang-php language-php php">add_action(&#039;mepr_account_nav&#039;, function($action) {
    $active = (isset($_GET[&#039;action&#039;]) &amp;&amp; $_GET[&#039;action&#039;] === &#039;user-commented&#039;) ? &#039;mepr-active-nav-tab&#039; : &#039;&#039;;
    echo &#039;&lt;span class=&quot;mepr-nav-item user-commented &#039; . esc_attr($active) . &#039;&quot;&gt;&#039;;
    echo &#039;&lt;a href=&quot;/account/?action=user-commented&quot;&gt;我的留言&lt;/a&gt;&#039;;
    echo &#039;&lt;/span&gt;&#039;;
});</code></pre>
<h3 class="wp-block-heading">顯示留言文章列表內容</h3>
<ul>
<li>先取得當前用戶留言過的文章</li>
<li>無留言時顯示提示文字</li>
<li>有留言時列出文章縮圖、分類、留言數與標題連結</li>
<li>若文章有額外權限設定，顯示鎖頭圖示</li>
</ul>
<pre><code class="lang-php language-php php">add_action(&#039;mepr_account_nav_content&#039;, function($action) {
    if ($action !== &#039;user-commented&#039;) {
        return;
    }

    $current_user_id = get_current_user_id();
    $commented_posts = ks_get_user_commented_posts($current_user_id);

    if (empty($commented_posts)) {
        echo &#039;&lt;div class=&quot;no-comments-message&quot;&gt;&#039;;
        echo &#039;
&lt;p&gt;尚無留言過的文章&lt;/p&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;
        return;
    }

    echo &lt;&lt;&lt;HTML
&lt;h2 class=&quot;follow-title&quot;&gt;
&lt;svg width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;path d=&quot;M8 7C8 6.44772 8.44772 6 9 6H23C23.5523 6 24 6.44772 24 7V25.1827C24 25.5564 23.6051 25.798 23.2724 25.6279L17.3659 22.6075C16.5081 22.1689 15.4919 22.1689 14.6341 22.6075L8.72764 25.6279C8.39495 25.798 8 25.5564 8 25.1827V7Z&quot; fill=&quot;#131314&quot; stroke=&quot;#131314&quot; stroke-width=&quot;1.5&quot;/&gt;
&lt;path d=&quot;M15.8153 9.44399C15.8837 9.27973 16.1163 9.27973 16.1847 9.44399L17.5401 12.7029C17.5689 12.7721 17.634 12.8194 17.7088 12.8254L21.227 13.1075C21.4044 13.1217 21.4763 13.343 21.3411 13.4587L18.6606 15.7549C18.6037 15.8037 18.5788 15.8802 18.5962 15.9532L19.4151 19.3864C19.4564 19.5594 19.2682 19.6962 19.1163 19.6035L16.1043 17.7637C16.0402 17.7246 15.9598 17.7246 15.8957 17.7637L12.8837 19.6035C12.7318 19.6962 12.5436 19.5594 12.5849 19.3864L13.4038 15.9532C13.4212 15.8802 13.3963 15.8037 13.3394 15.7549L10.6589 13.4587C10.5237 13.343 10.5957 13.1217 10.773 13.1075L14.2912 12.8254C14.366 12.8194 14.4311 12.7721 14.4599 12.7029L15.8153 9.44399Z&quot; fill=&quot;white&quot;/&gt;
&lt;/svg&gt;
我的留言
&lt;/h2&gt;
HTML;

    echo &#039;&lt;div class=&quot;my-comments-list&quot;&gt;&#039;;

    foreach ($commented_posts as $post) {
        $post_id = $post-&gt;ID;

        $thumbnail = has_post_thumbnail($post_id) ? get_the_post_thumbnail($post_id, &#039;medium&#039;, array(&#039;class&#039; =&gt; &#039;post-thumb&#039;, &#039;loading&#039; =&gt; &#039;lazy&#039;)) : &#039;&lt;div class=&quot;post-thumb post-thumb-default&quot;&gt;&lt;/div&gt;&#039;;

        $categories = get_the_category($post_id);
        $category_name = (!empty($categories)) ? $categories[0]-&gt;name : &#039;文章&#039;;

        $comment_count = ks_get_user_comment_count_for_post($post_id, $current_user_id);

        $post_title = get_the_title($post_id);
        $post_link = get_permalink($post_id);
        $has_access = get_field(&#039;ks_post_access&#039;, $post_id);

        echo &#039;&lt;div class=&quot;comment-post-item&quot;&gt;&#039;;
        echo &#039;&lt;div class=&quot;post-thumb-container&quot;&gt;&#039;;
        echo $thumbnail;
        echo &#039;&lt;div class=&quot;fi-badge&quot;&gt;&lt;span class=&quot;fi-tag&quot;&gt;&#039; . esc_html($category_name) . &#039;&lt;/span&gt;&#039;;
        if ($has_access) {
            echo &#039;&lt;span class=&quot;fi-access-icon&quot;&gt;&lt;svg width=&quot;10&quot; height=&quot;14&quot; viewBox=&quot;0 0 10 14&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;&lt;path d=&quot;M5 0C6.65685 0 8 1.34315 8 3V5H9C9.55228 5 10 5.44772 10 6V13C10 13.5523 9.55229 14 9 14H1C0.447715 14 4.02663e-09 13.5523 0 13V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0ZM5.17969 6.86328C5.10631 6.71493 4.8948 6.71508 4.82129 6.86328L4.16504 8.19238C4.13591 8.25128 4.07967 8.29225 4.01465 8.30176L2.54785 8.51465C2.38396 8.53863 2.31885 8.74079 2.4375 8.85645L3.49805 9.89062C3.54512 9.93657 3.56678 10.0025 3.55566 10.0674L3.30566 11.5273C3.27776 11.6906 3.44903 11.8153 3.5957 11.7383L4.90723 11.0488C4.96546 11.0183 5.03554 11.0182 5.09375 11.0488L6.40527 11.7383C6.55191 11.8152 6.72321 11.6906 6.69531 11.5273L6.44434 10.0674C6.43323 10.0026 6.45503 9.93656 6.50195 9.89062L7.56348 8.85645C7.68218 8.74074 7.61618 8.5385 7.45215 8.51465L5.98633 8.30176C5.92122 8.2923 5.86411 8.25136 5.83496 8.19238L5.17969 6.86328ZM5 1C3.89543 1 3 1.89543 3 3V5H7V3C7 1.89543 6.10457 1 5 1Z&quot; fill=&quot;white&quot;/&gt;&lt;/svg&gt;&lt;/span&gt;&#039;;
        }
        echo &#039;&lt;/div&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;

        echo &#039;&lt;div class=&quot;post-content-area&quot;&gt;&#039;;
        echo &#039;&lt;h3 class=&quot;post-title-link&quot;&gt;&#039;;
        echo &#039;&lt;a href=&quot;&#039; . esc_url($post_link) . &#039;&quot;&gt;&#039; . esc_html($post_title) . &#039;&lt;/a&gt;&#039;;
        echo &#039;&lt;/h3&gt;&#039;;
        echo &#039;&lt;div class=&quot;post-meta-info&quot;&gt;&#039;;
        echo &#039;&lt;span class=&quot;comment-count-info&quot;&gt;&#039; . $comment_count . &#039; 則留言&lt;/span&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;

        echo &#039;&lt;/div&gt;&#039;;
    }

    echo &#039;&lt;/div&gt;&#039;;
});</code></pre>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<ul>
<li>可結合 AJAX 實現分頁或動態載入，避免一次載入大量文章造成頁面卡頓</li>
<li>留言數量可用快取機制減少資料庫查詢次數</li>
<li>依需求擴充篩選條件，如文章分類、日期區間等</li>
<li>針對會員權限做更細緻的存取控制，提升安全性</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php   

/**
 * 獲取用戶留言過的文章ID陣列
 */
function ks_get_user_commented_post_ids( $user_id ) {
    global $wpdb;

    $query = $wpdb-&gt;prepare(
        &quot;SELECT DISTINCT comment_post_ID FROM {$wpdb-&gt;comments} WHERE user_id = %d&quot;,
        $user_id
    );

    $results = $wpdb-&gt;get_col( $query );
    return array_map( &#039;intval&#039;, $results );
}

/**
 * 獲取用戶留言過的文章
 */
function ks_get_user_commented_posts( $user_id = 0 ) {
    if ( ! $user_id ) {
        $user_id = get_current_user_id();
    }

    if ( ! $user_id ) {
        return array();
    }

    $commented_post_ids = ks_get_user_commented_post_ids( $user_id );

    if ( empty( $commented_post_ids ) ) {
        return array();
    }

    $valid_post_ids = array();
    foreach ($commented_post_ids as $post_id) {
        $post = get_post($post_id);
        if ($post &amp;&amp; $post-&gt;post_status === &#039;publish&#039; &amp;&amp; $post-&gt;post_type === &#039;post&#039;) {
            $valid_post_ids[] = $post_id;
        }
    }

    if (empty($valid_post_ids)) {
        return array();
    }

    $posts = get_posts( array(
        &#039;post_type&#039;      =&gt; &#039;post&#039;,
        &#039;post_status&#039;    =&gt; &#039;publish&#039;,
        &#039;posts_per_page&#039; =&gt; -1,
        &#039;post__in&#039;       =&gt; $valid_post_ids,
        &#039;orderby&#039;        =&gt; &#039;date&#039;,
        &#039;order&#039;          =&gt; &#039;DESC&#039;
    ) );

    return $posts;
}

/**
 * 獲取用戶在特定文章的留言數量
 */
function ks_get_user_comment_count_for_post( $post_id, $user_id = 0 ) {
    if ( ! $user_id ) {
        $user_id = get_current_user_id();
    }

    if ( ! $user_id ) {
        return 0;
    }

    global $wpdb;

    $count = $wpdb-&gt;get_var( $wpdb-&gt;prepare(
        &quot;SELECT COUNT(*) FROM {$wpdb-&gt;comments} WHERE comment_post_ID = %d AND user_id = %d&quot;,
        $post_id,
        $user_id
    ) );

    return intval($count);
}

// MemberPress 會員後台整合
add_action(&#039;mepr_account_nav&#039;, function($action) {
    $active = (isset($_GET[&#039;action&#039;]) &amp;&amp; $_GET[&#039;action&#039;] === &#039;user-commented&#039;) ? &#039;mepr-active-nav-tab&#039; : &#039;&#039;;
    echo &#039;&lt;span class=&quot;mepr-nav-item user-commented &#039; . esc_attr($active) . &#039;&quot;&gt;&#039;;
    echo &#039;&lt;a href=&quot;/account/?action=user-commented&quot;&gt;我的留言&lt;/a&gt;&#039;;
    echo &#039;&lt;/span&gt;&#039;;
});

add_action(&#039;mepr_account_nav_content&#039;, function($action) {
    if ($action !== &#039;user-commented&#039;) {
        return;
    }

    $current_user_id = get_current_user_id();
    $commented_posts = ks_get_user_commented_posts($current_user_id);

    if (empty($commented_posts)) {
        echo &#039;&lt;div class=&quot;no-comments-message&quot;&gt;&#039;;
        echo &#039;
&lt;p&gt;尚無留言過的文章&lt;/p&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;
        return;
    }

    echo &lt;&lt;&lt;HTML
&lt;h2 class=&quot;follow-title&quot;&gt;
&lt;svg width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;path d=&quot;M8 7C8 6.44772 8.44772 6 9 6H23C23.5523 6 24 6.44772 24 7V25.1827C24 25.5564 23.6051 25.798 23.2724 25.6279L17.3659 22.6075C16.5081 22.1689 15.4919 22.1689 14.6341 22.6075L8.72764 25.6279C8.39495 25.798 8 25.5564 8 25.1827V7Z&quot; fill=&quot;#131314&quot; stroke=&quot;#131314&quot; stroke-width=&quot;1.5&quot;/&gt;
&lt;path d=&quot;M15.8153 9.44399C15.8837 9.27973 16.1163 9.27973 16.1847 9.44399L17.5401 12.7029C17.5689 12.7721 17.634 12.8194 17.7088 12.8254L21.227 13.1075C21.4044 13.1217 21.4763 13.343 21.3411 13.4587L18.6606 15.7549C18.6037 15.8037 18.5788 15.8802 18.5962 15.9532L19.4151 19.3864C19.4564 19.5594 19.2682 19.6962 19.1163 19.6035L16.1043 17.7637C16.0402 17.7246 15.9598 17.7246 15.8957 17.7637L12.8837 19.6035C12.7318 19.6962 12.5436 19.5594 12.5849 19.3864L13.4038 15.9532C13.4212 15.8802 13.3963 15.8037 13.3394 15.7549L10.6589 13.4587C10.5237 13.343 10.5957 13.1217 10.773 13.1075L14.2912 12.8254C14.366 12.8194 14.4311 12.7721 14.4599 12.7029L15.8153 9.44399Z&quot; fill=&quot;white&quot;/&gt;
&lt;/svg&gt;
我的留言
&lt;/h2&gt;
HTML;

    echo &#039;&lt;div class=&quot;my-comments-list&quot;&gt;&#039;;

    foreach ($commented_posts as $post) {
        $post_id = $post-&gt;ID;

        $thumbnail = &#039;&#039;;
        if (has_post_thumbnail($post_id)) {
            $thumbnail = get_the_post_thumbnail($post_id, &#039;medium&#039;, array(
                &#039;class&#039; =&gt; &#039;post-thumb&#039;,
                &#039;loading&#039; =&gt; &#039;lazy&#039;
            ));
        } else {
            $thumbnail = &#039;&lt;div class=&quot;post-thumb post-thumb-default&quot;&gt;&lt;/div&gt;&#039;;
        }

        $categories = get_the_category($post_id);
        $category_name = (!empty($categories)) ? $categories[0]-&gt;name : &#039;文章&#039;;

        $comment_count = ks_get_user_comment_count_for_post($post_id, $current_user_id);

        $post_title = get_the_title($post_id);
        $post_link = get_permalink($post_id);
        $has_access = get_field(&#039;ks_post_access&#039;, $post_id);

        echo &#039;&lt;div class=&quot;comment-post-item&quot;&gt;&#039;;

        echo &#039;&lt;div class=&quot;post-thumb-container&quot;&gt;&#039;;
        echo $thumbnail;
        echo &#039;&lt;div class=&quot;fi-badge&quot;&gt;
        &lt;span class=&quot;fi-tag&quot;&gt;&#039; . esc_html($category_name) . &#039;&lt;/span&gt;&#039;;
                if ($has_access) { 
                    echo &#039;&lt;span class=&quot;fi-access-icon&quot;&gt;&lt;svg width=&quot;10&quot; height=&quot;14&quot; viewBox=&quot;0 0 10 14&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
            &lt;path d=&quot;M5 0C6.65685 0 8 1.34315 8 3V5H9C9.55228 5 10 5.44772 10 6V13C10 13.5523 9.55229 14 9 14H1C0.447715 14 4.02663e-09 13.5523 0 13V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0ZM5.17969 6.86328C5.10631 6.71493 4.8948 6.71508 4.82129 6.86328L4.16504 8.19238C4.13591 8.25128 4.07967 8.29225 4.01465 8.30176L2.54785 8.51465C2.38396 8.53863 2.31885 8.74079 2.4375 8.85645L3.49805 9.89062C3.54512 9.93657 3.56678 10.0025 3.55566 10.0674L3.30566 11.5273C3.27776 11.6906 3.44903 11.8153 3.5957 11.7383L4.90723 11.0488C4.96546 11.0183 5.03554 11.0182 5.09375 11.0488L6.40527 11.7383C6.55191 11.8152 6.72321 11.6906 6.69531 11.5273L6.44434 10.0674C6.43323 10.0026 6.45503 9.93656 6.50195 9.89062L7.56348 8.85645C7.68218 8.74074 7.61618 8.5385 7.45215 8.51465L5.98633 8.30176C5.92122 8.2923 5.86411 8.25136 5.83496 8.19238L5.17969 6.86328ZM5 1C3.89543 1 3 1.89543 3 3V5H7V3C7 1.89543 6.10457 1 5 1Z&quot; fill=&quot;white&quot;/&gt;&lt;/svg&gt;&lt;/span&gt;&#039;; 
                }
       echo &#039;&lt;/div&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;

        echo &#039;&lt;div class=&quot;post-content-area&quot;&gt;&#039;;
        echo &#039;&lt;h3 class=&quot;post-title-link&quot;&gt;&#039;;
        echo &#039;&lt;a href=&quot;&#039; . esc_url($post_link) . &#039;&quot;&gt;&#039; . esc_html($post_title) . &#039;&lt;/a&gt;&#039;;
        echo &#039;&lt;/h3&gt;&#039;;
        echo &#039;&lt;div class=&quot;post-meta-info&quot;&gt;&#039;;
        echo &#039;&lt;span class=&quot;comment-count-info&quot;&gt;&#039; . $comment_count . &#039; 則留言&lt;/span&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;
        echo &#039;&lt;/div&gt;&#039;;

        echo &#039;&lt;/div&gt;&#039;;
    }

    echo &#039;&lt;/div&gt;&#039;;
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-user-commented-posts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>自訂 MemberPress 貨幣格式為無小數點整數顯示</title>
		<link>https://piglife.tw/technical-notes/memberpress-currency-integer-format/</link>
					<comments>https://piglife.tw/technical-notes/memberpress-currency-integer-format/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Sun, 14 Dec 2025 22:20:27 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[filter]]></category>
		<category><![CDATA[MemberPress]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/memberpress-currency-integer-format/</guid>

					<description><![CDATA[示範如何利用 MemberPress 的 mepr_format_currency filter，自...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在使用 MemberPress 這類會員付費外掛時，預設的貨幣格式通常會包含小數點，像是「$123.00」。有些商業需求或設計風格希望價格以整數顯示，去除小數點部分。本文示範如何透過 WordPress 的 filter 機制，將 MemberPress 的貨幣格式調整成無小數點的整數顯示，並保留貨幣符號原本的位置。</p>
<p>這篇文章適合對 WordPress 外掛開發有基礎認識，並且需要自訂 MemberPress 付款顯示格式的工程師或自學者。</p>
<h2 class="wp-block-heading">透過 filter 自訂貨幣格式</h2>
<p>MemberPress 提供 <code>mepr_format_currency</code> 這個 filter，可以攔截並修改貨幣字串的輸出。這段程式碼利用匿名函式接收三個參數：</p>
<ul>
<li><code>$rstr</code>：原始格式化後的貨幣字串</li>
<li><code>$number</code>：數字部分</li>
<li><code>$show_symbol</code>：是否顯示貨幣符號</li>
</ul>
<p>接著使用 MemberPress 內建的 <code>MeprUtils::format_currency_float</code> 函式，將數字四捨五入到 0 位小數，轉成字串。</p>
<pre><code class="lang-php language-php php">$no_decimals = (string) MeprUtils::format_currency_float((float) $number, 0);</code></pre>
<h2 class="wp-block-heading">保留貨幣符號原本位置</h2>
<p>為了不破壞原本貨幣符號的位置（有些貨幣符號在前，有些在後），程式碼判斷原字串 <code>$rstr</code> 是否以貨幣符號開頭：</p>
<pre><code class="lang-php language-php php">if (strpos($rstr, $symbol) === 0) {
  return $symbol . $no_decimals;
} else {
  return $no_decimals . &#039; &#039; . $symbol;
}</code></pre>
<p>這樣可以維持「$123」或「123 $」的格式一致性。</p>
<h2 class="wp-block-heading">實務應用與注意事項</h2>
<ul>
<li>適用於價格不需要小數點顯示的場景，例如整數金額訂閱方案。</li>
<li>若未來有多種貨幣符號，需確認 <code>MeprOptions::fetch()</code> 取得的符號是否正確。</li>
<li>由於是使用匿名函式，若需移除此 filter，需注意無法直接使用 <code>remove_filter</code>。</li>
</ul>
<h2 class="wp-block-heading">延伸優化方向</h2>
<ul>
<li>可根據不同貨幣類型動態調整小數位數。</li>
<li>加入千分位分隔符號增強可讀性。</li>
<li>支援多語系貨幣格式。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
// functions.php 或自訂外掛
add_filter(&#039;mepr_format_currency&#039;, function($rstr, $number, $show_symbol) {
  $mepr_options = MeprOptions::fetch();

  // 將數字改成 0 位小數（四捨五入）
  $no_decimals = (string) MeprUtils::format_currency_float((float) $number, 0);

  if (!$show_symbol) {
    return $no_decimals;
  }

  // 簡單依據原字串是否以貨幣符號開頭，維持原有符號位置
  $symbol = $mepr_options-&gt;currency_symbol;
  if (strpos($rstr, $symbol) === 0) {
    // 原本是「$123.00」這種前置符號
    return $symbol . $no_decimals;
  } else {
    // 原本是「123.00 $」這種後置符號
    return $no_decimals . &#039; &#039; . $symbol;
  }
}, 10, 3);</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/memberpress-currency-integer-format/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 會員中心新增追蹤文章清單功能實作</title>
		<link>https://piglife.tw/technical-notes/wordpress-user-follow-posts/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-user-follow-posts/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Sat, 13 Dec 2025 22:20:48 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[Flexbox]]></category>
		<category><![CDATA[MemberPress]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-user-follow-posts/</guid>

					<description><![CDATA[介紹如何在 WordPress 會員中心新增追蹤文章分頁，並以 Flexbox 版型呈現使用者收藏的...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>這段 PHP 程式碼用於在 WordPress 會員中心（使用 MemberPress 插件）新增一個「追蹤文章」的分頁，讓使用者可以快速查看自己收藏或追蹤的文章列表。適合需要在會員系統中提供個人化內容管理的網站開發者與自學者。</p>
<h2 class="wp-block-heading">新增會員中心導覽標籤</h2>
<p>透過 <code>mepr_account_nav</code> action，程式碼在會員中心導覽列新增一個名為「追蹤文章」的標籤。當使用者點擊此標籤時，會帶入 <code>action=user-follow</code> 的 URL 參數，並且根據當前頁面狀態動態加上 CSS 樣式以標示為當前選中狀態。</p>
<pre><code class="lang-php language-php php">add_action(&#039;mepr_account_nav&#039;, function($action){
  $active = (isset($_GET[&#039;action&#039;]) &amp;&amp; $_GET[&#039;action&#039;] === &#039;user-follow&#039;) ? &#039;mepr-active-nav-tab&#039; : &#039;&#039;;
  echo &#039;&lt;span class=&quot;mepr-nav-item user-follow &#039; . esc_attr($active) . &#039;&quot;&gt;&lt;a href=&quot;/account/?action=user-follow&quot;&gt;追蹤文章&lt;/a&gt;&lt;/span&gt;&#039;;
});</code></pre>
<h2 class="wp-block-heading">顯示追蹤文章清單內容</h2>
<p>使用 <code>mepr_account_nav_content</code> action 來判斷是否為 <code>user-follow</code> 頁面，若是則取得該會員收藏的文章 ID。此處利用 <code>get_user_favorites</code> 函式（需額外插件或自訂函式支持）取得文章列表，並以 Flexbox 版型顯示。</p>
<h3 class="wp-block-heading">文章資料與版型處理</h3>
<ul>
<li>透過 <code>get_the_post_thumbnail</code> 取得文章縮圖，若無縮圖則顯示預設佔位元素。</li>
<li>文章分類顯示為角標，若無分類則顯示「文章」。</li>
<li>若文章有特定欄位 <code>ks_post_access</code> 標示有權限，則在角標旁顯示鎖頭圖示。</li>
</ul>
<h3 class="wp-block-heading">文章列表輸出範例</h3>
<pre><code class="lang-php language-php php">foreach ($post_ids as $post_id) {
  $has_access = get_field(&#039;ks_post_access&#039;, $post_id);
  $thumb_html = get_the_post_thumbnail($post_id, &#039;medium_large&#039;, [&#039;class&#039; =&gt; &#039;fi-img&#039;, &#039;loading&#039;=&gt;&#039;lazy&#039;, &#039;decoding&#039;=&gt;&#039;async&#039;]);
  if(empty($thumb_html)) {
    $thumb_html = &#039;&lt;div class=&quot;fi-img fi-img--placeholder&quot; aria-hidden=&quot;true&quot;&gt;&lt;/div&gt;&#039;;
  }
  $cats = get_the_category($post_id);
  $badge = (!empty($cats) &amp;&amp; !is_wp_error($cats)) ? $cats[0]-&gt;name : &#039;文章&#039;;
  $permalink = get_permalink($post_id);
  $title = get_the_title($post_id);

  echo &#039;&lt;a class=&quot;follow-item&quot; href=&quot;&#039; . esc_url($permalink) . &#039;&quot;&gt;&#039;
       . &#039;&lt;div class=&quot;fi-thumb&quot;&gt;&#039; . $thumb_html
       . &#039;&lt;div class=&quot;fi-badge&quot;&gt;&lt;span class=&quot;fi-tag&quot;&gt;&#039; . esc_html($badge) . &#039;&lt;/span&gt;&#039;;
  if ($has_access) {
    echo &#039;&lt;span class=&quot;fi-access-icon&quot;&gt;(鎖頭 SVG)&lt;/span&gt;&#039;;
  }
  echo &#039;&lt;/div&gt;&lt;/div&gt;&#039;
       . &#039;&lt;div class=&quot;fi-content&quot;&gt;&lt;h3 class=&quot;fi-title&quot;&gt;&#039; . esc_html($title) . &#039;&lt;/h3&gt;&lt;/div&gt;&#039;
       . &#039;&lt;/a&gt;&#039;;
}</code></pre>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<p>此功能適合用於會員制網站，讓使用者能方便管理自己感興趣的文章。可進一步結合 AJAX 技術實現無刷新更新，或加入取消追蹤功能提升互動性。若文章有付費限制，鎖頭圖示能明確提示使用者權限狀態。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li><code>get_user_favorites</code> 函式需確認是否有安裝相應收藏功能插件，否則會回傳空陣列。</li>
<li>權限欄位 <code>ks_post_access</code> 為自訂欄位，需確保文章有正確設定。</li>
<li>SVG 圖示直接內嵌於 HTML，方便自訂樣式與顯示，避免外部資源依賴。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">// Account &gt; 追蹤清單 tab
add_action(&#039;mepr_account_nav&#039;, function($action){
  $active = (isset($_GET[&#039;action&#039;]) &amp;&amp; $_GET[&#039;action&#039;] === &#039;user-follow&#039;) ? &#039;mepr-active-nav-tab&#039; : &#039;&#039;;
  echo &#039;&lt;span class=&quot;mepr-nav-item user-follow &#039; . esc_attr($active) . &#039;&quot;&gt;&lt;a href=&quot;/account/?action=user-follow&quot;&gt;追蹤文章&lt;/a&gt;&lt;/span&gt;&#039;;
});

// Account &gt; 追蹤清單內容：Flex 版型
add_action(&#039;mepr_account_nav_content&#039;, function($action){
  if($action !== &#039;user-follow&#039;) return;

  // Favorite Posts
  $filters = [
    &#039;post_type&#039; =&gt; [&#039;post&#039;],
    &#039;status&#039;    =&gt; [&#039;publish&#039;],
  ];
  $post_ids = function_exists(&#039;get_user_favorites&#039;)
    ? get_user_favorites(get_current_user_id(), null, $filters)
    : [];

  if (empty($post_ids)) {
    echo &#039;
&lt;p&gt;尚無追蹤的文章&lt;/p&gt;&#039;;
    return;
  }

  echo &#039;&lt;h2 class=&quot;follow-title&quot;&gt;&lt;svg width=&quot;32&quot; height=&quot;32&quot; viewBox=&quot;0 0 32 32&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;path d=&quot;M8 7C8 6.44772 8.44772 6 9 6H23C23.5523 6 24 6.44772 24 7V25.1827C24 25.5564 23.6051 25.798 23.2724 25.6279L17.3659 22.6075C16.5081 22.1689 15.4919 22.1689 14.6341 22.6075L8.72764 25.6279C8.39495 25.798 8 25.5564 8 25.1827V7Z&quot; fill=&quot;#131314&quot; stroke=&quot;#131314&quot; stroke-width=&quot;1.5&quot;/&gt;
&lt;path d=&quot;M15.8153 9.44399C15.8837 9.27973 16.1163 9.27973 16.1847 9.44399L17.5401 12.7029C17.5689 12.7721 17.634 12.8194 17.7088 12.8254L21.227 13.1075C21.4044 13.1217 21.4763 13.343 21.3411 13.4587L18.6606 15.7549C18.6037 15.8037 18.5788 15.8802 18.5962 15.9532L19.4151 19.3864C19.4564 19.5594 19.2682 19.6962 19.1163 19.6035L16.1043 17.7637C16.0402 17.7246 15.9598 17.7246 15.8957 17.7637L12.8837 19.6035C12.7318 19.6962 12.5436 19.5594 12.5849 19.3864L13.4038 15.9532C13.4212 15.8802 13.3963 15.8037 13.3394 15.7549L10.6589 13.4587C10.5237 13.343 10.5957 13.1217 10.773 13.1075L14.2912 12.8254C14.366 12.8194 14.4311 12.7721 14.4599 12.7029L15.8153 9.44399Z&quot; fill=&quot;white&quot;/&gt;
&lt;/svg&gt;我的追蹤文章&lt;/h2&gt;&#039;;

  echo &#039;&lt;div class=&quot;follow-list&quot;&gt;&#039;;
  foreach ($post_ids as $post_id) {
    // 檢查是否有 access 權限
    $has_access = get_field(&#039;ks_post_access&#039;, $post_id);

    // 縮圖
    $thumb_html = get_the_post_thumbnail(
      $post_id,
      &#039;medium_large&#039;,
      [&#039;class&#039; =&gt; &#039;fi-img&#039;, &#039;loading&#039;=&gt;&#039;lazy&#039;, &#039;decoding&#039;=&gt;&#039;async&#039;]
    );
    if(empty($thumb_html)){
      $thumb_html = &#039;&lt;div class=&quot;fi-img fi-img--placeholder&quot; aria-hidden=&quot;true&quot;&gt;&lt;/div&gt;&#039;;
    }

    // 角標：第一個分類，無則顯示「文章」
    $cats  = get_the_category($post_id);
    $badge = (!empty($cats) &amp;&amp; !is_wp_error($cats)) ? $cats[0]-&gt;name : &#039;文章&#039;;
    $permalink = get_permalink($post_id);
    $title     = get_the_title($post_id);

    echo &#039;&lt;a class=&quot;follow-item&quot; href=&quot;&#039; . esc_url($permalink) . &#039;&quot;&gt;&#039;
         . &#039;&lt;div class=&quot;fi-thumb&quot;&gt;&#039;
         .   $thumb_html .   
        &#039;&lt;div class=&quot;fi-badge&quot;&gt;
        &lt;span class=&quot;fi-tag&quot;&gt;&#039; . esc_html($badge) . &#039;&lt;/span&gt;&#039;;
        // 如果有 access 權限，顯示鎖頭圖標 
        if ($has_access) { 
            echo &#039;&lt;span class=&quot;fi-access-icon&quot;&gt;&lt;svg width=&quot;10&quot; height=&quot;14&quot; viewBox=&quot;0 0 10 14&quot; fill=&quot;none&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;path d=&quot;M5 0C6.65685 0 8 1.34315 8 3V5H9C9.55228 5 10 5.44772 10 6V13C10 13.5523 9.55229 14 9 14H1C0.447715 14 4.02663e-09 13.5523 0 13V6C0 5.44772 0.447715 5 1 5H2V3C2 1.34315 3.34315 0 5 0ZM5.17969 6.86328C5.10631 6.71493 4.8948 6.71508 4.82129 6.86328L4.16504 8.19238C4.13591 8.25128 4.07967 8.29225 4.01465 8.30176L2.54785 8.51465C2.38396 8.53863 2.31885 8.74079 2.4375 8.85645L3.49805 9.89062C3.54512 9.93657 3.56678 10.0025 3.55566 10.0674L3.30566 11.5273C3.27776 11.6906 3.44903 11.8153 3.5957 11.7383L4.90723 11.0488C4.96546 11.0183 5.03554 11.0182 5.09375 11.0488L6.40527 11.7383C6.55191 11.8152 6.72321 11.6906 6.69531 11.5273L6.44434 10.0674C6.43323 10.0026 6.45503 9.93656 6.50195 9.89062L7.56348 8.85645C7.68218 8.74074 7.61618 8.5385 7.45215 8.51465L5.98633 8.30176C5.92122 8.2923 5.86411 8.25136 5.83496 8.19238L5.17969 6.86328ZM5 1C3.89543 1 3 1.89543 3 3V5H7V3C7 1.89543 6.10457 1 5 1Z&quot; fill=&quot;white&quot;/&gt;&lt;/svg&gt;&lt;/span&gt;&#039;; 
    }

            echo &#039;&lt;/div&gt;&#039;;

    echo   &#039;&lt;/div&gt;&#039;
         . &#039;&lt;div class=&quot;fi-content&quot;&gt;&#039;
         .   &#039;&lt;h3 class=&quot;fi-title&quot;&gt;&#039; . esc_html($title) . &#039;&lt;/h3&gt;&#039;
         . &#039;&lt;/div&gt;&#039;
         . &#039;&lt;/a&gt;&#039;;
  }
  echo &#039;&lt;/div&gt;&#039;;
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-user-follow-posts/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>利用短碼控制內容顯示：結合 MemberPress 權限判斷的實作範例</title>
		<link>https://piglife.tw/technical-notes/mepr-access-view-shortcode/</link>
					<comments>https://piglife.tw/technical-notes/mepr-access-view-shortcode/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Thu, 11 Dec 2025 22:20:38 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[MemberPress]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/mepr-access-view-shortcode/</guid>

					<description><![CDATA[介紹如何使用 WordPress 短碼結合 MemberPress 存取規則，根據會員權限動態顯示不...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 網站中，常見需求是根據使用者的會員權限來控制內容的顯示。這段 PHP 程式碼示範如何利用短碼（shortcode）結合 MemberPress 的存取規則，根據不同的權限狀態呈現不同內容。適合已有 WordPress 和 MemberPress 基礎，想自訂內容權限顯示的工程師或自學者。</p>
<h2 class="wp-block-heading">短碼設計與參數說明</h2>
<p>此短碼名稱為 <code>ks_mepr_access_view</code>，接受一個參數 <code>type</code>，用來決定顯示的內容類型：</p>
<ul>
<li><code>public</code>：公開內容，無需會員權限</li>
<li><code>member</code>：會員專屬內容，需通過 MemberPress 規則授權</li>
<li><code>noaccess</code>：未授權會員或訪客看到的內容</li>
</ul>
<p>使用範例：</p>
<pre><code class="lang-php language-php php">[ks_mepr_access_view type=&quot;member&quot;]會員專屬內容[/ks_mepr_access_view]</code></pre>
<h2 class="wp-block-heading">權限判斷流程解析</h2>
<h3 class="wp-block-heading">取得文章與規則</h3>
<pre><code class="lang-php language-php php">$post = get_post();
$rules = MeprRule::get_rules($post);</code></pre>
<p>先取得當前文章物件，再用 MemberPress API 取得該文章綁定的存取規則。</p>
<h3 class="wp-block-heading">判斷使用者登入與權限</h3>
<pre><code class="lang-php language-php php">$user = wp_get_current_user();
$is_logged_in = is_user_logged_in();
$has_access = false;

if ($is_logged_in &amp;&amp; !empty($rules)) {
    $member = new MeprUser($user-&gt;ID);
    foreach ($rules as $rule) {
        if ($member-&gt;has_access_from_rule($rule-&gt;ID)) {
            $has_access = true;
            break;
        }
    }
}</code></pre>
<p>確認使用者是否登入，且文章有設定規則，接著檢查使用者是否符合任一規則，若符合即標記為有存取權限。</p>
<h3 class="wp-block-heading">根據條件回傳內容</h3>
<pre><code class="lang-php language-php php">if (!$has_rule &amp;&amp; $type === &#039;public&#039;) {
    return do_shortcode($content); // 無保護，公開內容
}

if ($has_rule &amp;&amp; $has_access &amp;&amp; $type === &#039;member&#039;) {
    return do_shortcode($content); // 有保護且有權限，顯示會員內容
}

if ($has_rule &amp;&amp; !$has_access &amp;&amp; $type === &#039;noaccess&#039;) {
    return do_shortcode($content); // 有保護但無權限，顯示無權限內容
}

return &#039;&#039;;</code></pre>
<p>依據是否有規則、是否有權限，以及 <code>type</code> 參數決定是否顯示內容，未符合條件則回傳空字串。</p>
<h2 class="wp-block-heading">實務應用與擴充建議</h2>
<ul>
<li>可用於文章、頁面或自訂文章類型中，靈活控制不同會員等級的內容呈現。</li>
<li>可結合前端樣式或 JavaScript，提升使用者體驗，例如顯示提示訊息或引導登入。</li>
<li>若需更複雜的權限判斷，可擴充短碼參數，或搭配其他會員系統 API。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>確保 MemberPress 已正確設定存取規則並綁定文章。</li>
<li>使用者權限判斷依賴 MemberPress API，若插件更新需確認相容性。</li>
<li>短碼內容內仍可使用其他短碼，<code>do_shortcode</code> 可確保內嵌短碼正常解析。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
function ks_mepr_access_view_shortcode($atts, $content = null) {
    $atts = shortcode_atts([
        &#039;type&#039; =&gt; &#039;public&#039;, // 可為 public, member, noaccess
    ], $atts);

    $post = get_post();
    if (!$post) {
        return &#039;&#039;;
    }

    $type = strtolower($atts[&#039;type&#039;]);
    $rules = MeprRule::get_rules($post);
    $has_rule = !empty($rules);

    $user = wp_get_current_user();
    $is_logged_in = is_user_logged_in();
    $has_access = false;

    if ($is_logged_in &amp;&amp; $has_rule) {
        $member = new MeprUser($user-&gt;ID);
        foreach ($rules as $rule) {
            if ($member-&gt;has_access_from_rule($rule-&gt;ID)) {
                $has_access = true;
                break;
            }
        }
    }

    // 顯示條件
    if (!$has_rule &amp;&amp; $type === &#039;public&#039;) {
        return do_shortcode($content); // 沒有保護 &rarr; 公開
    }

    if ($has_rule &amp;&amp; $has_access &amp;&amp; $type === &#039;member&#039;) {
        return do_shortcode($content); // 有保護+通過 &rarr; 會員
    }

    if ($has_rule &amp;&amp; !$has_access &amp;&amp; $type === &#039;noaccess&#039;) {
        return do_shortcode($content); // 有保護+沒通過（不論登入與否）&rarr; 無權限
    }

    return &#039;&#039;;
}
add_shortcode(&#039;ks_mepr_access_view&#039;, &#039;ks_mepr_access_view_shortcode&#039;);</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/mepr-access-view-shortcode/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 單篇文章瀏覽次數計數器實作</title>
		<link>https://piglife.tw/technical-notes/wordpress-post-view-counter/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-post-view-counter/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Thu, 11 Dec 2025 02:50:35 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[post meta]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[wp_head]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-post-view-counter/</guid>

					<description><![CDATA[本文介紹如何在 WordPress 單篇文章頁面利用自訂欄位與 wp_head 鉤子實作瀏覽次數計數...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在許多網站中，追蹤文章的瀏覽次數是一個常見需求，可以用來分析內容受歡迎程度或作為推薦依據。這段 PHP 程式碼示範如何在 WordPress 單篇文章頁面中，利用文章自訂欄位（post meta）記錄瀏覽次數，適合有基本 WordPress 開發經驗的工程師或自學者。</p>
<h2 class="wp-block-heading">透過 wp_head 鉤子更新瀏覽次數</h2>
<p>WordPress 提供了多種鉤子（hook）讓開發者在不同時機點執行程式碼。這裡選擇使用 <code>wp_head</code>，代表每次載入頁面時會執行計數函式。</p>
<pre><code class="lang-php language-php php">add_action( &#039;wp_head&#039;, &#039;ks_track_post_views&#039; );</code></pre>
<p>這樣可以確保在文章頁面載入時，瀏覽次數會即時更新。</p>
<h2 class="wp-block-heading">計數函式邏輯解析</h2>
<p>函式 <code>ks_track_post_views</code> 主要做以下幾件事：</p>
<ol>
<li>使用 <code>is_single()</code> 判斷目前是否為單篇文章頁，避免在首頁或列表頁誤計數。</li>
<li>取得當前文章 ID。</li>
<li>從文章自訂欄位 <code>ks_post_views</code> 讀取目前瀏覽次數。</li>
<li>若已有數值，將其轉為整數後加一；若無則初始化為 1。</li>
<li>更新文章自訂欄位，寫入新的瀏覽次數。</li>
</ol>
<pre><code class="lang-php language-php php">function ks_track_post_views() {
    if ( is_single() ) {
        global $post;
        $post_id = $post-&gt;ID;

        $views = get_post_meta( $post_id, &#039;ks_post_views&#039;, true );
        $views = $views ? intval( $views ) + 1 : 1;
        update_post_meta( $post_id, &#039;ks_post_views&#039;, $views );
    }
}</code></pre>
<p>這段程式碼利用 WordPress 內建的 <code>get_post_meta</code> 和 <code>update_post_meta</code> 函式操作文章自訂欄位，避免直接操作資料庫，維持良好的相容性。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li><strong>緩存問題</strong>：如果網站使用快取機制，可能會導致瀏覽次數無法即時更新，建議搭配 Ajax 或其他非快取方式更新計數。</li>
<li><strong>防止重複計數</strong>：目前邏輯無法區分同一用戶多次刷新，若需更精確的統計，可結合 Cookie 或 Session 控制。</li>
<li><strong>效能考量</strong>：大量文章同時更新 meta 可能影響效能，必要時可考慮批次更新或使用專門的統計外掛。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>使用 <code>wp_head</code> 會在每次頁面載入時執行，若頁面包含大量資源，可能會稍微增加伺服器負擔。</li>
<li>確保文章自訂欄位名稱不與其他外掛或主題衝突，這裡使用 <code>ks_post_views</code> 作為前綴。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php

function ks_track_post_views() {
    if ( is_single() ) {
        global $post;
        $post_id = $post-&gt;ID;

        $views = get_post_meta( $post_id, &#039;ks_post_views&#039;, true );
        $views = $views ? intval( $views ) + 1 : 1;
        update_post_meta( $post_id, &#039;ks_post_views&#039;, $views );
    }
}
add_action( &#039;wp_head&#039;, &#039;ks_track_post_views&#039; );
</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-post-view-counter/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>自訂 WordPress 登入頁面 Logo 與連結設定教學</title>
		<link>https://piglife.tw/technical-notes/wordpress-custom-login-logo/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-custom-login-logo/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:23:35 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[Logo自訂]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-custom-login-logo/</guid>

					<description><![CDATA[說明如何透過 WordPress 鉤子自訂登入頁面 Logo 圖片、連結與標題，提升品牌一致性與使用...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>WordPress 預設的登入頁面 Logo 通常是 WordPress 自身標誌，對於品牌一致性或客製化需求來說，常常需要替換成自家 Logo。這篇文章針對有基本 PHP 與 WordPress 鉤子（Hook）概念的工程師，說明如何透過簡單的程式碼修改登入頁面 Logo 及其連結與標題。</p>
<h2 class="wp-block-heading">修改登入頁面 Logo 的方法</h2>
<p>WordPress 提供了 <code>login_enqueue_scripts</code> 動作鉤子，可以在登入頁面載入自訂 CSS。利用這個鉤子，我們可以注入 CSS 來改變 Logo 背景圖。</p>
<h3 class="wp-block-heading">主要程式碼片段</h3>
<pre><code class="lang-php language-php php">function ks_custom_login_logo() { ?&gt;
    &lt;style type=&quot;text/css&quot;&gt;
        body.login div#login h1 a {
            background-image: url(&#039;/wp-content/uploads/login-logo.svg&#039;);
            width: 200px;        /* 依你的 SVG 比例調整 */
            height: 80px;        /* 依你的 SVG 比例調整 */
            background-size: contain;
            background-repeat: no-repeat;
            background-position: center;
            padding-bottom: 0;
        }
    &lt;/style&gt;
&lt;?php }
add_action( &#039;login_enqueue_scripts&#039;, &#039;ks_custom_login_logo&#039; );</code></pre>
<p>這段程式碼將登入頁面中 Logo 的背景圖片改為指定路徑的 SVG，並調整尺寸與顯示方式，確保圖片完整且置中。</p>
<h2 class="wp-block-heading">自訂 Logo 點擊連結與標題</h2>
<p>預設 WordPress 登入頁 Logo 點擊會導向 wordpress.org，且滑鼠移到 Logo 上會顯示 &#8220;Powered by WordPress&#8221;。若想改為導向自己網站首頁，並顯示網站名稱，可以使用以下兩個過濾器：</p>
<pre><code class="lang-php language-php php">function ks_custom_login_logo_url( $url ) {
    return home_url( &#039;/&#039; );
}
add_filter( &#039;login_headerurl&#039;, &#039;ks_custom_login_logo_url&#039; );

function ks_custom_login_logo_title( $title ) {
    return get_bloginfo( &#039;name&#039; );
}
add_filter( &#039;login_headertext&#039;, &#039;ks_custom_login_logo_title&#039; );</code></pre>
<p>這樣使用者點擊 Logo 就會回到網站首頁，且滑鼠懸停時顯示網站名稱，提升品牌辨識度。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li><strong>Logo 圖片路徑</strong>：請確保 <code>background-image</code> 的 URL 是正確且可存取的路徑，通常放在 WordPress 媒體庫或主題資料夾。</li>
<li><strong>尺寸調整</strong>：根據 Logo 圖檔比例調整 <code>width</code> 與 <code>height</code>，避免變形。</li>
<li><strong>響應式設計</strong>：可進一步用媒體查詢調整不同裝置的尺寸。</li>
<li><strong>安全性</strong>：避免在登入頁面加入過多外部資源，確保載入速度與安全。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>若修改後 Logo 不顯示，請檢查圖片路徑是否正確，並清除瀏覽器快取。</li>
<li>使用 SVG 時，確保檔案安全且無惡意程式碼。</li>
<li>若有快取外掛或 CDN，請同步清除快取。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
// 修改 WordPress 登入頁 Logo
function ks_custom_login_logo() { ?&gt;
    &lt;style type=&quot;text/css&quot;&gt;
        body.login div#login h1 a {
            background-image: url(&#039;/wp-content/uploads/login-logo.svg&#039;);
            width: 200px;        /* 依你的 SVG 比例調整 */
            height: 80px;        /* 依你的 SVG 比例調整 */
            background-size: contain;
            background-repeat: no-repeat;
            background-position: center;
            padding-bottom: 0;
        }
    &lt;/style&gt;
&lt;?php }
add_action( &#039;login_enqueue_scripts&#039;, &#039;ks_custom_login_logo&#039; );

// 自訂 Logo 點擊連結為首頁
function ks_custom_login_logo_url( $url ) {
    return home_url( &#039;/&#039; );
}
add_filter( &#039;login_headerurl&#039;, &#039;ks_custom_login_logo_url&#039; );

// 自訂 Logo 滑鼠懸停標題為網站名稱
function ks_custom_login_logo_title( $title ) {
    return get_bloginfo( &#039;name&#039; );
}
add_filter( &#039;login_headertext&#039;, &#039;ks_custom_login_logo_title&#039; );</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-custom-login-logo/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>自訂 WordPress 摘要結尾文字的簡易方法</title>
		<link>https://piglife.tw/technical-notes/wordpress-excerpt-more-customize/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-excerpt-more-customize/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:23:14 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[excerpt_more]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-excerpt-more-customize/</guid>

					<description><![CDATA[介紹如何使用 WordPress 的 excerpt_more 過濾器簡單自訂文章摘要結尾文字，提升...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 主題開發或客製化時，常會需要調整文章摘要（excerpt）結尾的文字，預設為「[&#8230;]」的顯示可能不符合設計需求。這段 PHP 程式碼示範如何用最簡單的方式修改摘要結尾文字，適合有基本 WordPress 開發經驗的工程師或自學者。</p>
<h2 class="wp-block-heading">為什麼要修改摘要結尾文字</h2>
<p>WordPress 預設的摘要結尾通常是「[&#8230;]」，這是用來表示文章內容被截斷。若想讓網站風格更統一或更符合閱讀體驗，可以透過過濾器（filter）自訂這段文字。</p>
<h2 class="wp-block-heading">透過 add_filter 修改 excerpt_more</h2>
<p>關鍵在於 WordPress 提供的 <code>excerpt_more</code> 過濾器，可以攔截並改寫摘要結尾文字。以下是程式碼片段：</p>
<pre><code class="lang-php language-php php">add_filter(&#039;excerpt_more&#039;, function($more) {
    return &#039;&hellip;&#039;;
});</code></pre>
<p>這段程式碼使用匿名函式（Closure）直接回傳一個單一的省略符號「…」，取代預設的「[&#8230;]」。</p>
<h3 class="wp-block-heading">程式碼解析</h3>
<ul>
<li><code>add_filter</code> 是 WordPress 用來掛載過濾器的函式。</li>
<li><code>&#039;excerpt_more&#039;</code> 是過濾器名稱，專門用來修改摘要後綴。</li>
<li>匿名函式接收 <code>$more</code> 參數（原本的結尾文字），但這裡直接忽略它，回傳自訂字串。</li>
</ul>
<p>這種寫法簡潔且易於維護，尤其適合只想快速改變摘要結尾的情境。</p>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<ul>
<li>可將回傳字串改成其他符號或文字，例如「繼續閱讀」搭配連結。</li>
<li>若需要更複雜的摘要控制，可結合 <code>excerpt_length</code> 過濾器調整摘要長度。</li>
<li>注意部分主題或外掛可能會覆寫此過濾器，需確認優先權設定。</li>
</ul>
<h2 class="wp-block-heading">常見問題</h2>
<ul>
<li>
<p><strong>為什麼修改後沒效果？</strong>
可能是主題直接使用自訂函式產生摘要，未使用 WordPress 預設的 <code>the_excerpt()</code> 函式。</p>
</li>
<li>
<p><strong>如何同時修改摘要長度？</strong>
可搭配 <code>excerpt_length</code> 過濾器一起使用。</p>
</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
add_filter(&#039;excerpt_more&#039;, function($more) {
    return &#039;&hellip;&#039;;
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-excerpt-more-customize/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
