靜態區塊會把內容寫死在文章裡,但很多區塊不能這樣做,它的內容要每次載入時從資料開取得,像是一個要根據後台欄位顯示的服務內容介紹、一段要抓最新文章的列表,這種時候就要用動態區塊。
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.js 的 save 從「回傳 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 現算的,改了輸出結構也不會有靜態區塊那種區塊驗證失敗的問題,這是動態區塊的一大好處。
同樣要做「一張欄位可調的卡片」,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.js 的 edit 裡,用核心的 @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/