我們開始來做第一個 WordPress 自訂區塊。重新複習一下靜態區塊的概念:它的輸出固定寫死在文章內容裡,不需要每次載入時用 PHP 重算,這是最單純的區塊型態,先從它建立心智模型,後面的動態區塊、互動區塊都是在這個基礎上長出來的。
做區塊第一個要認識的檔案是 block.json。它是整個區塊的「單一事實來源」:區塊叫什麼名字、屬於哪個分類、有哪些可調的屬性(attributes)、支援哪些功能、要載入哪些 JS/CSS,全部宣告在這一個檔案裡。WordPress、編輯器、建置工具都讀它。
這跟傳統做法差很多。以前你要做一個「可重複用的內容」,得在 functions.php 註冊 shortcode、另外寫 CSS、enqueue JS,資訊散在三四個地方。block.json 把這些收成一個宣告檔,改一個地方就好。
一個自訂區塊不是單一檔案,可以用 blocks/ 底下的一個資料夾來整理,裡面幾個檔案各司其職。以這個 eyebrow 為例:
blocks/eyebrow/
├── block.json # 宣告:名字、屬性、要載入哪些 JS/CSS(就是剛剛那個「單一事實來源」)
├── index.js # 編輯器端的 JavaScript:edit 怎麼顯示、save 存成什麼
└── style.css # 這個區塊的樣式
以下逐步介紹每個檔案的內容。首先是 block.json :
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "block-theme/eyebrow",
"title": "Eyebrow",
"category": "theme",
"textdomain": "block-theme",
"attributes": {
"text": { "type": "string", "source": "html", "selector": "span" }
},
"supports": { "html": false },
"editorScript": "file:./index.js",
"style": "file:./style.css"
}
這段不長,但每個欄位都有它的職責,快速掃一遍:
$schema:指向 block.json 的官方結構定義,讓編輯器和 IDE 能幫你自動補全、檢查有沒有寫錯欄位。純輔助,不影響功能。apiVersion:用哪一代的 Block API,現在一律填 3(最新、支援 iframe 編輯器那套)。name:區塊的唯一識別,格式是「命名空間/區塊名」。這裡是 block-theme/eyebrow,命名空間用主題的 slug,避免跟別人的區塊撞名。title:在編輯器插入器(inserter)裡顯示的名稱,使用者實際看到、搜尋到的就是它。category:這個區塊歸在插入器的哪一個分類。theme 是我們自己註冊的分類,把主題的自訂區塊整合在一起。textdomain:i18n 的翻譯網域,要跟主題一致(block-theme),區塊裡的 __() 字串才翻得到。attributes:這個區塊「能存哪些值」。這裡宣告一個 text,型別是字串。supports:設定這個區塊要不要擁有核心內建的能力(對齊、顏色⋯,Day 19 專講)。這裡 "html": false 是關掉「以 HTML 原始碼編輯」那顆選項,免得使用者手改壞結構。editorScript:這個區塊在「編輯器端」要載入的 JS,file:./index.js 指的就是同資料夾的 index.js。style:前後台都會載入的樣式檔,file:./style.css。其中 attributes.text 用了 "source": "html" 加 "selector": "span",意思是這個值直接從存下來的 HTML 的 <span> 裡撈,不另外存資料庫,這正是「靜態」的特徵。
回頭看剛剛 block.json 的最後兩行:"editorScript": "file:./index.js" 和 "style": "file:./style.css",它們就是在指「同一個資料夾裡的 index.js 和 style.css」。換句話說,block.json 是索引,真正的行為寫在它指到的 index.js。所以接下來要出現的那段 JS,不是憑空冒出來的,它就是 index.js 的內容。(靜態區塊就這三個檔案;等 Day 20 做動態區塊時,資料夾會多一個 render.php,把渲染交給 PHP。)
接下來看最關鍵的 index.js。每個區塊裡面都有兩個要寫的函式:edit 和 save。
edit:決定「在後台編輯器裡」長什麼樣、怎麼操作。使用者在後台看到能點能改的行為都定義在這個函式。save:決定「存進資料庫後給前台看」的 HTML 長什麼樣。靜態區塊的重點就在這裡:save 回傳的 markup 會直接寫進文章內容,前台載入時原封不動輸出,不經過 PHP。這是 index.js 的完整內容,用 RichText 做一個可就地編輯文字的 eyebrow:
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
return (
<RichText
{ ...blockProps }
tagName="span"
value={ attributes.text }
onChange={ ( text ) => setAttributes( { text } ) }
placeholder="輸入標語…"
/>
);
},
save( { attributes } ) {
const blockProps = useBlockProps.save();
return <RichText.Save { ...blockProps } tagName="span" value={ attributes.text } />;
},
} );
先看開頭三個 import 和註冊:
import { registerBlockType } from '@wordpress/blocks';:從核心的 blocks 套件拿 registerBlockType,這是「向 WordPress 註冊一個區塊」的函式。import { useBlockProps, RichText } from '@wordpress/block-editor';:拿兩個編輯器工具。useBlockProps 幫區塊外層套上該有的 class 和屬性;RichText 是可即時編輯的文字元件。import metadata from './block.json';:把同資料夾的 block.json 讀進來,等下直接用它的 name,不用再手打一次區塊名字。registerBlockType( metadata.name, { … } );:用 block.json 裡的名字(block-theme/eyebrow)註冊區塊;第二個參數這個物件,裝的就是 edit 和 save 兩個函式。再看 edit,它決定「編輯器裡」怎麼顯示:
edit( { attributes, setAttributes } ) {:編輯器要畫這個區塊時會呼叫它,並遞給你目前的 attributes(現在的值)和 setAttributes(改值的函式)。const blockProps = useBlockProps();:產生區塊外層必要的 props(class、data 屬性等)。少了它,區塊在編輯器裡會缺少框選、對齊這些預設行為。<RichText … />:回傳畫面,就是一個可直接打字的文字欄位。裡面幾個屬性:
{ ...blockProps }:把上一行那組 props 組合進去。tagName="span":這段文字實際渲染成 <span>。value={ attributes.text }:顯示目前存的文字。onChange={ ( text ) => setAttributes( { text } ) }:使用者每改一次字,就把新文字寫回 attributes.text。placeholder="輸入標語…":還沒有內容時顯示的提示字。最後看 save,它決定儲存到資料庫給前台顯示的 HTML:
save( { attributes } ) {:要存檔、輸出前台時呼叫,只需要 attributes,不需要 setAttributes(前台不編輯)。const blockProps = useBlockProps.save();:save 專用的 blockProps,套在真正要寫進資料庫的那段 HTML 上。return <RichText.Save … />:用 RichText.Save 把 attributes.text 輸出成 <span>,這段 HTML 會直接寫進文章內容。一句話總結:edit 裡用 RichText 讓使用者就地打字,改動透過 setAttributes 存進 attributes.text;save 用 RichText.Save 把同一個值輸出成 <span>,兩邊 tagName 一致,這樣前後台的呈現結果才會一樣。
靜態區塊最大的好處是快:前台不跑 PHP,就是一段現成 HTML,效能最好,適合「內容完成後就固定」的東西:一段標語、一個引言、一塊 CTA 文案。
但它有個著名的陷阱:block validation(區塊驗證)。因為 markup 存死在資料庫,一旦你改了 save 的輸出結構(多包一層 div、換個 class),舊文章裡存的舊 HTML 就跟新的 save 對不上,編輯器會跳「此區塊包含非預期或無效的內容」。這時除非用 deprecation 保留舊版,或是讓 AI 幫你批次轉換。
這也是為什麼:當內容需要「每次載入重算」時我們會改用動態區塊,把渲染交給 PHP 就沒有這個問題。
實務上我不會手刻上面每個檔案,我會跟 Claude Code 說:「幫我在 blocks/eyebrow/ 做一個靜態區塊,一個 text 屬性,用 RichText 輸入元件,輸出成 span。」它會照 block.json + edit/save 的慣例把檔案生齊,因為 wp-block-themes 技能裡就寫著這套慣例。
你要做的是看懂它生的對不對,而你今天已經有這個能力了。第一個區塊做出來了。下一篇我們把焦點放在區塊編輯器的設定功能,我們只要在 block.json 動一個欄位:supports,就能立刻擁有完整的對齊、顏色、間距的控制項,完全不用自己寫。
文章目錄:https://oberonlai.blog/category/2026-ithome/