WordPress のカスタムブロックは、block.json に設定を書いただけでは完成しません。編集画面でどう見えるか、投稿にどんな HTML を保存するかは、JavaScript 側の registerBlockType() に渡す edit と save が決めています。この記事では index.js の基本形から、edit と save の役割の違い、両者に欠かせない useBlockProps の使い方、そして出力がズレたときに出るブロック検証エラーの仕組みまでを解説します。WordPress 6 系、apiVersion 3 を前提としています。
目次
registerBlockType はブロックの動きを JavaScript 側で登録する関数
カスタムブロックの登録は、block.json と registerBlockType() の2段構えになっています。block.json はブロックの名前やアイコン、属性の定義といった静的な情報を宣言するファイルで、PHP 側の register_block_type() が読み込みます。一方で「編集画面にどんな UI を出すか」「保存時にどんな HTML を返すか」は JSON では書けません。この振る舞いの部分を担当するのが、@wordpress/blocks パッケージの registerBlockType() です。
この関数は第1引数にブロック名、第2引数に設定オブジェクトを取ります。設定オブジェクトの中心になるのが edit と save の2つで、実質この2つを書くために registerBlockType() を呼んでいると言ってもよいくらいです。
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import './style.scss';
// 第1引数は block.json の name(= webool/notice-box)
registerBlockType( metadata.name, {
edit: Edit,
save,
} );
ブロック名を文字列で直接書かず、import metadata from './block.json' として metadata.name を渡しているのがポイントです。名前を2か所に書くと片方だけ直したときにずれてしまい、PHP 側で登録されたブロックと JavaScript 側の実装が結び付かなくなります。block.json を単一の情報源にしておけば、この種の食い違いは起きません。registerBlockType() の第1引数には読み込んだメタデータのオブジェクトそのものを渡すこともできますが、block.json が PHP 側からも読まれている構成では metadata.name を渡す形が分かりやすいでしょう。
なお、この index.js は block.json の editorScript に "file:./index.js" として指定されているファイルです。エディターを開いたときだけ読み込まれ、そこで registerBlockType() が実行されてブロックがエディターに登録される、という流れになります。
edit は編集画面の見た目、save は保存される HTML
edit と save はどちらも要素を返す関数ですが、その出力が使われる場所はまったく別です。edit が返したものは編集画面にだけ表示され、投稿には保存されません。逆に save が返したものは投稿本文の HTML として保存され、フロント側で読者が目にするのはこちらです。
| 項目 | edit | save |
|---|---|---|
| 出力の行き先 | ブロックエディターの画面 | 投稿本文(データベース)に保存される HTML |
| 実行されるタイミング | 編集中、値が変わるたびに再描画される | ブロックの保存時と、投稿を開いたときの検証時 |
| 受け取れる主な引数 | attributes / setAttributes / isSelected / clientId / context など | attributes / innerBlocks |
| 状態や副作用 | useState などの React フックを使える | 使えない。属性から出力が一意に決まる書き方にする |
| ルート要素に付ける属性 | useBlockProps() | useBlockProps.save() |
edit は React コンポーネントとして扱われるので、useState でパネルの開閉を管理したり、isSelected を見て選択中だけツールバーを出したりと、編集のための作り込みが自由にできます。プレースホルダーの文言や入力欄の枠線など、編集中にだけ必要なものはすべてここに書きます。
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const { heading } = attributes;
// 編集画面のルート要素に付ける属性をまとめて受け取る
const blockProps = useBlockProps( { className: 'notice-box' } );
return (
<div { ...blockProps }>
<RichText
tagName="p"
className="notice-box__heading"
value={ heading }
onChange={ ( value ) => setAttributes( { heading: value } ) }
placeholder="見出しを入力"
/>
</div>
);
}
これに対して save は、属性を受け取って HTML を組み立てるだけの素朴な関数です。setAttributes は渡されませんし、フックも使えません。入力欄の代わりに RichText.Content のような「保存用」のコンポーネントを使い、編集のための余計な要素は出力しません。
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { heading } = attributes;
// 保存する HTML のルート要素に付ける属性
const blockProps = useBlockProps.save( { className: 'notice-box' } );
return (
<div { ...blockProps }>
<RichText.Content
tagName="p"
className="notice-box__heading"
value={ heading }
/>
</div>
);
}
save で気を付けたいのは、同じ属性からは必ず同じ HTML が返るように書くことです。現在時刻やランダムな値を混ぜると、保存したときの HTML と、次に投稿を開いて検証したときの HTML が一致しなくなります。「毎回変わる内容を出したい」場合は、あとで触れる動的ブロックの出番です。
useBlockProps がルート要素に付けてくれるもの
useBlockProps() は @wordpress/block-editor が提供する関数で、ブロックのルート要素に必要な属性をまとめて生成してくれます。返り値のオブジェクトを { ...blockProps } のようにスプレッド構文で展開すると、クラス名やスタイルが一度に適用されます。apiVersion 2 以降のブロックでは、この呼び出しが事実上必須です。
具体的には、次のようなものがルート要素に付きます。
| 付与されるもの | 内容 |
|---|---|
| ブロック固有のクラス | wp-block-webool-notice-box のように、ブロック名から作られるクラス名(supports.className が true のとき) |
| 追加 CSS クラス | サイドバーの「追加 CSS クラス」欄でユーザーが入力したクラス名 |
| 配置のクラス | supports.align を有効にしたときの alignwide / alignfull など |
| 色・余白などのクラスとスタイル | supports の色や余白の設定から生成される has-…-background-color といったクラスと、style 属性のインラインスタイル |
| アンカー | supports.anchor で入力された id 属性 |
| エディター内部の属性 | 編集画面側でのみ付与される、ブロックの選択やドラッグ操作に必要な参照や属性 |
自分で付けたいクラスがあるときは、引数にオブジェクトを渡します。useBlockProps( { className: 'notice-box' } ) と書けば、自動生成されるクラスと notice-box がマージされた状態で返ってきます。className を自前で属性に書き足すのではなく、この引数経由で渡すのが基本です。
edit では useBlockProps()、save では useBlockProps.save()
同じ役割の関数ですが、edit と save で呼び分けます。edit の中では useBlockProps() を、save の中では useBlockProps.save() を使います。edit 側はエディターの状態にアクセスするフックなので、ブロックの選択状態やドラッグ操作に必要な情報も一緒に返します。save 側は保存する HTML に必要な属性だけを返す純粋な関数で、エディター専用の属性は含まれません。
間違えて save の中で useBlockProps() を呼ぶと、エディターの文脈がない場所でフックを呼ぶことになり、正しく動きません。save と書いてあるファイルでは必ず .save() を付ける、と覚えておくと迷いません。
付け忘れると何が起きるか
edit で useBlockProps() を付け忘れると、ブロックがエディターに表示はされるのに、クリックしても選択できない、ツールバーが出ない、ドラッグで並べ替えられない、といった状態になります。見た目のスタイルが当たらないだけでなく、ブロックとしての基本操作ができなくなるので、「なぜかこのブロックだけ選択できない」と感じたら真っ先に確認したいポイントです。
save で useBlockProps.save() を付け忘れた場合は、保存される HTML に wp-block-… のクラスや、色・余白の設定から生成されたクラスとスタイルが出力されません。エディター上ではきれいに見えているのに、フロントで見ると色も余白も効いていない、という食い違いはこれが原因のことがよくあります。supports にいくら項目を足しても、受け取り口である useBlockProps.save() がなければ結果は HTML に反映されないのです。
save の出力が変わるとブロック検証エラーになる
カスタムブロック開発で最も戸惑うのが、投稿を開いた瞬間に出る「このブロックには、想定されていないエラーが含まれています」という表示、いわゆるブロック検証エラー(Block validation failed)です。これは save の性質を知っていると理解しやすくなります。
保存済みの HTML と save の出力を突き合わせている
投稿を開くと、WordPress は本文に保存されている HTML を読み込み、同時にブロックの属性を使って save をもう一度実行します。そして保存済みの HTML と、いま生成された HTML を比較します。ここが一致していれば正常なブロックとして表示され、食い違っていれば検証エラーになります。エディターがブロックの内容を安全に扱えるかどうかを、この照合で確認しているわけです。
したがって、開発中に save のマークアップを変えると、以前その形で保存した投稿は当然ながら一致しなくなります。要素を div から section に変えた、クラス名を付け直した、あとから useBlockProps.save() を追加した——どれもエラーの引き金になります。edit のほうをいくら書き換えてもエラーにならないのは、edit の出力が保存対象ではないからです。
edit と save でクラス名がズレているケース
もう一つよくあるのが、edit と save で構造を微妙に変えてしまうパターンです。block.json の attributes で source と selector を指定している場合、値は保存済み HTML の中の該当要素から読み戻されます。save 側のクラス名を notice-box__heading から notice-box__title に変えると、selector が指す要素が見つからず属性が空になり、その空の値で生成した HTML が保存済みのものと一致せずエラーになります。
edit と save は別の関数ですが、selector で参照する要素とクラス名だけは意識して揃えておく必要があります。編集用の装飾は useBlockProps の引数や内側の要素で調整し、値を持つ要素そのものの構造は両者で共通にしておくと安全です。
エラーが出たときの直し方
開発中でまだ公開記事に使っていないブロックなら、そのブロックを削除して挿入し直すのが一番早い方法です。エディターに表示される「ブロックのリカバリーを試行」を押せば、いまの save の出力で保存し直してくれるので、こちらでも復旧できます。ただしリカバリーは属性を読み戻せた範囲でしか内容を保てないため、構造を大きく変えた直後は中身が欠けることもあります。
すでに公開済みの投稿で使っているブロックの場合は、古い形の save を deprecated として登録し、旧 HTML も読める状態にしたうえで新しい形へ移行します。いずれにしても、attributes と save は公開後に気軽に変えられないものだと考えて設計しておくのが安心です。
save に null を返して PHP に任せる方法もある
最新の投稿一覧のように、表示するたびに内容が変わるブロックでは、そもそも HTML を投稿に保存するという発想が合いません。こうした場合は save で null を返し、フロント側の HTML は PHP に組み立てさせます。これが動的ブロックと呼ばれる作り方です。
export default function save() {
// HTML は保存せず、フロントの出力は PHP 側に任せる
return null;
}
save を書かずに省略した場合も同じ扱いになります。投稿本文にはブロックコメントと属性の JSON だけが残り、比較対象の HTML がないので検証エラーとは無縁になります。表示のたびに内容を作り直せるので、記事の追加やデータの更新が自動で反映されるのも利点です。
PHP 側の出力は block.json の render に指定したファイルが担当します。動的ブロックでは属性を HTML から読み戻せないため、attributes に source を指定せず、ブロックコメントに保存される形で定義する点にだけ注意してください。詳しい書き方は動的ブロック側の話題になるので、ここでは「save には null という選択肢がある」ことを押さえておけば十分です。
まとめ
registerBlockType() は、block.json で宣言したブロックに振る舞いを与える関数です。index.js では metadata.name を第1引数に渡し、edit と save を登録します。edit は編集画面の見た目を担当する React コンポーネントで、フックや setAttributes が使えます。save は投稿に保存される HTML を返す純粋な関数で、同じ属性からは常に同じ出力になるように書きます。
どちらのルート要素にも useBlockProps を展開しておくことが欠かせません。edit では useBlockProps()、save では useBlockProps.save() を使い、クラス名や配置、supports で設定された色や余白をまとめて受け取ります。付け忘れると、編集画面ではブロックを選択できず、フロントでは設定した色や余白が出力されません。そして save の出力を変更すると保存済みの HTML と食い違い、ブロック検証エラーになります。表示のたびに内容が変わるブロックを作りたいときは、save に null を返して PHP に描画を任せる動的ブロックを検討してみてください。