iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

靜態區塊會把內容寫死在文章裡,但很多區塊不能這樣做,它的內容要每次載入時從資料開取得,像是一個要根據後台欄位顯示的服務內容介紹、一段要抓最新文章的列表,這種時候就要用動態區塊。

save 不輸出 HTML,改由 PHP 在前台渲染時即時生成,這一篇用一個真實的 service-card 區塊,比較兩條做動態區塊的路:ACF 欄位與原生 attributes。

動態區塊的骨架

先把兩種區塊的資料夾擺在一起看,差別一眼就出來。左邊是昨天那個靜態的 eyebrow,右邊是今天的動態 service-card:

靜態(eyebrow)             動態(service-card)
blocks/eyebrow/            blocks/service-card/
├── block.json             ├── block.json
├── index.js               ├── index.js
└── style.css              ├── render.php   ← 多這一個
                           └── style.css

多出來的就是 render.php,它正是「動態」的關鍵。對應到程式碼,動態區塊跟靜態的差別就落在兩處:block.json 多宣告一個 render 指向這支 PHP 檔,而 index.jssave 從「回傳 HTML」改成「回傳 null」,把畫面輸出整個讓給 PHP。先看 service-card 的 block.json

{
  "name": "block-theme/service-card",
  "attributes": {
    "number": { "type": "string", "default": "01" },
    "title":  { "type": "string" },
    "text":   { "type": "string" },
    "linkText": { "type": "string" },
    "url":    { "type": "string", "default": "#" }
  },
  "render": "file:./render.php"
}

render.php 負責把屬性變成 HTML,重點是每個輸出都要 escape:

$title = ( isset( $attributes['title'] ) ) ? $attributes['title'] : '';
$url   = ( isset( $attributes['url'] ) ) ? $attributes['url'] : '#';
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
  <span class="...__number"><?php echo esc_html( $attributes['number'] ); ?></span>
  <h3 class="...__title"><?php echo esc_html( $title ); ?></h3>
  <a class="...__link" href="<?php echo esc_url( $url ); ?>">…</a>
</div>

這段雖短,但它是幾乎每個動態區塊 render.php 都長一樣的骨架:先取值、再包外層、然後逐一 escape 輸出。拆開看:

  • $title = ( isset( $attributes['title'] ) ) ? $attributes['title'] : '';$attributes 就是編輯器存下來的那組值(number、title、url…),WordPress 渲染時會自動遞給 render.php。這行先用 isset 確認 title 有沒有值,有就用、沒有給空字串,避免直接取一個不存在的 key 跳出 warning。下面每個屬性都這樣取,url 取不到時預設 #
  • <?php echo get_block_wrapper_attributes(); ?>:核心函式,幫最外層那個 <div> 自動補上「一個區塊該有的」class 與屬性,包含區塊自己的 class、以及使用者在編輯器設定的對齊、顏色、間距等。你不用手寫 class="wp-block-…",交給它就對了。
  • esc_html( $attributes['number'] ) / esc_html( $title ):輸出純文字前先跳脫,把 <>& 這些字元轉成安全的 HTML 實體,擋掉有人在欄位裡塞 <script> 之類的內容。凡是輸出文字,一律包 esc_html()
  • esc_url( $url ):輸出網址前的跳脫,確保 href 是乾淨的 URL,順手擋掉 javascript: 這種危險協定。凡是輸出連結,一律包 esc_url()
  • class="...__number"...__title:用的是 BEM 命名(區塊__元素),對應 style.css 裡的樣式。這裡的 ... 只是行文精簡,實際是 wp-block-block-theme-service-card__number 這種完整名稱。

一句話記:render.php 的工作就是「把 $attributes 的值,安全地填進一段 HTML 樣板」。取值防呆、輸出跳脫,這兩件事做好,動態區塊的 render 就穩了。

因為 markup 是 PHP 現算的,改了輸出結構也不會有靜態區塊那種區塊驗證失敗的問題,這是動態區塊的一大好處。

兩條路:ACF 還是原生 attributes

同樣要做「一張欄位可調的卡片」,WordPress 生態有兩個主流做法。

做法一:ACF Blocks。 如果你來自傳統開發,這條最熟。用 acf_register_block_type() 註冊區塊,欄位在 ACF 的圖形介面拉一拉就好,render 樣板裡用 get_field('title') 取值。優點是欄位 UI 完全不用寫,ACF 幫你生好;缺點是綁一個外掛、資料進了 ACF 的儲存結構,而且它不是核心原生的路。

做法二:原生 attributes。 就是上面 service-card 的做法。屬性宣告在 block.json,編輯 UI 自己用 @wordpress/components 寫(TextControl、SelectControl…),render 交給 render.php。優點是零外掛相依、純核心,資料就存在區塊屬性裡;代價是欄位 UI 要自己寫一點。

那「自己寫欄位 UI」到底長怎樣?就是在 index.jsedit 裡,用核心的 @wordpress/components 把每個屬性接上一個輸入框。service-card 的 edit 精簡起來是這樣:

import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import ServerSideRender from '@wordpress/server-side-render';

edit( { attributes, setAttributes } ) {
  const blockProps = useBlockProps();
  return (
    <>
      <InspectorControls>
        <PanelBody title="Content">
          <TextControl
            label="title"
            value={ attributes.title }
            onChange={ ( v ) => setAttributes( { title: v } ) }
          />
          { /* number、text、linkText、url 各一個,寫法一樣 */ }
        </PanelBody>
      </InspectorControls>
      <div { ...blockProps }>
        <ServerSideRender block={ metadata.name } attributes={ attributes } />
      </div>
    </>
  );
},
save: () => null,

設計好的結果如下:

幾個重點:

  • InspectorControls:右側設定欄。放進去的東西會出現在編輯器右邊那塊面板,PanelBody 是可折疊的分組標題。
  • TextControl:一個文字輸入框。value 綁到 attributes.title 顯示目前的值,onChange 在使用者打字時用 setAttributes 把新值寫回屬性。每個屬性(number、text、linkText、url)就照這個模式各接一個,五個屬性就五個 control。
  • ServerSideRender:預覽用。它把目前的 attributes 丟回後端跑 render.php,直接在編輯器中央顯示「前台會長怎樣」。好處是編輯器和前台共用同一份 render,不會兩邊長不一樣。
  • save: () => null:再次強調,動態區塊的 save 回傳 null,畫面全交給 render.php

所以原生 attributes 的代價是每個欄位都要手動接一個 control,好消息是這種粗活最適合交給 AI 生,你只要講清楚有哪些欄位就好,至於右側欄這種編輯方式好不好用、跟「就地編輯」怎麼取捨,之後我們會再詳細分析。

拿傳統佈景主題類比:ACF Blocks 像你習慣的「ACF 自訂欄位」思路搬到區塊上;原生 attributes 則更接近「自己做一個乾淨的自訂功能」,多一點工但不需要依賴任何第三方外掛。

面向 ACF Blocks 原生 attributes
欄位 UI ACF 圖形介面生成 自己用 components 寫
相依 需要 ACF 外掛 純核心,零相依
資料存哪 ACF 的儲存結構 區塊屬性(文章內容)
適合 已重度用 ACF 的專案、要快 要交付乾淨、可散佈的主題

我的選擇與原因

因為 AI 我選了原生 attributes,而且我不想讓主題硬綁一個商業外掛,使用者少裝 ACF 就少一個依賴,而「自己寫欄位 UI」這件過去最煩的事,現在交給 AI 就好,我跟 Claude Code 說「service-card 要 number、title、text、linkText、url 五個屬性,側邊欄用對應的 TextControl 讓我改」,它照 wp-block-themes 的慣例把 edit.js 生齊,連 escape、text domain 都到位。

換句話說,過去選 ACF 常常是為了「省掉寫 UI 的工」,當這個工交給 AI 之後,方向就往「純核心、零相依」那靠攏,這也是我覺得 AI 時代 Block Theme 很吃香的另一個具體例子。

動態區塊的屬性是「存在這個區塊自己身上」的資料。如果我想讓區塊去吃「文章的自訂欄位」或是「網站的全域設定」這類外部資料來源呢?那就是下一篇我們要討論的:Block Binding,讓內容跟資料來源解耦。

文章目錄:https://oberonlai.blog/category/2026-ithome/


Hi, 我是 Oberon Lai,十多年前我從一個不懂程式的平面設計師,一頭栽進 WordPress 的世界。從佈景主題到外掛開發,從接案到自研產品,這段旅程讓我深刻理解:好的技術不只是寫出能跑的程式碼,而是真正解決人的問題。

我積極投入參與社群,公開演講紀錄如下:

我專精 WordPress 開發,從企業形象網站的設計與開發、佈景主題客製化,到既有網站的改版升級,提供完整的 WordPress 建置服務。開發面涵蓋外掛開發與維護、區塊編輯器(Gutenberg)客製區塊、ACF 與 Custom Post Type 的資料架構設計,以及 REST API 整合與 Multisite 多站架構建置。同時也協助網站效能改善與 SEO 調校、安全性檢測與強化,並導入自動化部署與版本控制流程,搭配長期的技術顧問與維運支援,讓網站上線後也能穩定運作。

亦提供 WooCommerce 商店的建置與設定,並串接綠界、LINE Pay、藍新等台灣主流金流。可依需求進行結帳頁面客製化、訂單狀態自動化流程設計,以及商品管理與庫存系統的客製開發;也支援 WooCommerce Subscription 定期定額、REST API 應用開發、報表與數據匯出等進階需求。此外,透過購物流程 UX 改善、電商網站效能調校與 HPOS 高效能訂單儲存相容開發,全面提升營運效率,並提供電商營運技術顧問服務。

AI 浪潮席捲而來,我選擇擁抱而非恐懼,我把 AI 融入開發工作流以及客戶的產品中,也持續累積「AI 看不見的部分」:真實踩坑經驗、最新漏洞情報那些只有第一線工程師才看得見的細節,如果你有任何 WordPress 的客製化需求或是 AI 開發相關的問題非常歡迎加入 LINE 官方帳號與我聯繫:

https://page.line.me/vrf7844t?oat_content=url&openQrModal=true

如果想要獲取 AI 開發實戰經驗也能訂閱我的電子報,每週五上午準時出刊:

https://oberonlai.blog/wordpress-newsletter/


上一篇
Block Supports 內建開箱即用的設定區塊設定欄位
下一篇
Block Binding:讓內容與資料來源解耦
系列文
從一句話到一個網站:用 Vibe Coding 開發 WordPress Block Theme 的 30 天40
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言