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/


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

尚未有邦友留言

立即登入留言