カスタムブロックを作っていると、「投稿を保存した時点の HTML を固定したくない」という場面が出てきます。最新の投稿一覧やログイン中のユーザー名のように、表示するたびに内容が変わるものは、JavaScript の save で HTML を保存するのではなく、表示のたびに PHP で組み立てる必要があります。これが動的ブロック(ダイナミックブロック)です。この記事では、block.json の render プロパティで render.php を指定する方法を中心に、render_callback との違い、render.php の中で使える変数、get_block_wrapper_attributes() の使い方までを解説します。
目次
静的ブロックと動的ブロックの違い
通常のカスタムブロック(静的ブロック)は、save 関数が返した HTML がそのまま投稿本文(post_content)に文字列として保存されます。フロントエンドではその保存済み HTML がそのまま出力されるため、表示は速い一方で、保存したあとに内容を変えることができません。ブロックのマークアップを修正しても、過去に保存された投稿は古い HTML のままです。
動的ブロックでは、save は null を返し、投稿本文にはブロックコメント(<!-- wp:webool/recent-posts {"numberOfPosts":5} /--> のような1行)だけが保存されます。実際の HTML はページが表示されるたびにサーバー側の PHP が生成します。この仕組みをサーバーサイドレンダリングと呼びます。
| 静的ブロック | 動的ブロック | |
|---|---|---|
| HTML を作る場所 | JavaScript の save | PHP(render.php など) |
| 投稿本文に保存される内容 | 完成した HTML | ブロックコメントと属性のみ |
| HTML を作るタイミング | 投稿を保存したとき | ページを表示するたび |
| あとからマークアップを変更 | 過去の投稿には反映されない | すべての投稿に即反映される |
block.json の render プロパティで render.php を指定する
WordPress 6.1 から、block.json に render プロパティが追加されました。ここに PHP ファイルを指定しておくと、そのファイルがブロックの描画テンプレートとして使われます。値は file:./render.php のように file: を付けたパスで書き、block.json があるディレクトリからの相対パスになります。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "webool/recent-posts",
"title": "最新投稿リスト",
"category": "widgets",
"icon": "list-view",
"textdomain": "webool",
"attributes": {
"numberOfPosts": {
"type": "number",
"default": 5
}
},
"supports": {
"color": {
"background": true,
"text": true
}
},
"editorScript": "file:./index.js",
"render": "file:./render.php"
}
ブロックの登録側は静的ブロックと同じで、block.json のあるディレクトリを register_block_type() に渡すだけです。render の指定は block.json を読み込む時点で解釈されるので、PHP 側に描画用のコードを書き足す必要はありません。
<?php
function webool_register_blocks() {
// build 内の block.json があるディレクトリを指定する
register_block_type( __DIR__ . '/build/recent-posts' );
}
add_action( 'init', 'webool_register_blocks' );
@wordpress/scripts(wp-scripts build)を使っている場合、block.json の render で指定した PHP ファイルはビルド時に build ディレクトリへコピーされます。src に置いた render.php を自分で build へコピーする必要はありません。
render.php の中で使える $attributes・$content・$block
render.php は、ブロックを表示するたびに読み込まれる PHP ファイルです。return で値を返すのではなく、ファイルの中で出力(echo)した内容がそのままブロックの HTML になります。このファイルの中では、次の3つの変数があらかじめ用意されています。
| 変数 | 中身 |
|---|---|
$attributes | ブロックの属性の配列。block.json の attributes で定義したキーが入り、値が無いものには default が使われる |
$content | ブロックの内側の HTML。InnerBlocks を使っている場合は、その子ブロックが描画された HTML が入る |
$block | ブロックそのものを表す WP_Block オブジェクト。$block->context で親から渡されたコンテキストなどを参照できる |
まずは属性を1つ受け取って表示するだけの、最小の render.php を見てみます。
<?php
/**
* このファイルの中では次の変数が使える
*
* @var array $attributes ブロックの属性
* @var string $content 内側の HTML
* @var WP_Block $block ブロックのインスタンス
*/
$number = $attributes['numberOfPosts'];
?>
<p>最新の投稿を <?php echo esc_html( $number ); ?> 件表示します。</p>
この時点でも動きますが、このままではブロックエディターで設定した文字色や背景色が反映されません。次の get_block_wrapper_attributes() がその役目を担います。
get_block_wrapper_attributes() でラッパー要素の属性を出力する
静的ブロックでは、save の中で useBlockProps.save() を展開することで supports 由来のクラスやインラインスタイルが出力されます。動的ブロックにはその save がないため、代わりに PHP の get_block_wrapper_attributes() を使います。この関数は class="..." style="..." のような属性の文字列を返すので、一番外側のタグの中でそのまま出力します。
<?php
// 独自のクラスを追加したいときは配列で渡す
$wrapper_attributes = get_block_wrapper_attributes(
array( 'class' => 'webool-recent-posts' )
);
?>
<div <?php echo $wrapper_attributes; ?>>
<p>ここに中身を書きます。</p>
</div>
引数に配列を渡すと、その内容が supports 由来の値と統合されます。class は既存のクラスに追加され、style も同様にまとめられます。戻り値はエスケープ済みの属性文字列なので、esc_attr() で包み直す必要はありません。また、この関数はブロックの描画中にだけ正しく動作するので、render.php や render_callback の外で呼んでも意図した値は得られません。
最新投稿の一覧を出力する実践例
ここまでの内容をまとめて、「最新の投稿を指定件数だけ並べる」ブロックを作ってみます。件数はブロックの属性 numberOfPosts から受け取り、投稿が1件も無いときは何も出力せずに終了します。render.php は途中で return すればそこで出力が止まるので、条件によって非表示にしたい場合に使えます。
<?php
$number = isset( $attributes['numberOfPosts'] ) ? (int) $attributes['numberOfPosts'] : 5;
$recent_posts = get_posts(
array(
'numberposts' => $number,
'post_status' => 'publish',
)
);
// 投稿が無いときは何も出力しない
if ( empty( $recent_posts ) ) {
return;
}
?>
<ul <?php echo get_block_wrapper_attributes( array( 'class' => 'webool-recent-posts' ) ); ?>>
<?php foreach ( $recent_posts as $recent_post ) : ?>
<li>
<a href="<?php echo esc_url( get_permalink( $recent_post ) ); ?>">
<?php echo esc_html( get_the_title( $recent_post ) ); ?>
</a>
<time><?php echo esc_html( get_the_date( '', $recent_post ) ); ?></time>
</li>
<?php endforeach; ?>
</ul>
ループ変数に $post ではなく $recent_post を使っているのは、WordPress のグローバル変数 $post を上書きして、後続のテンプレートの表示を壊してしまわないようにするためです。get_permalink() や get_the_title() は投稿オブジェクトを引数に取れるので、setup_postdata() を使わずにこの形で書けます。
save.js は null を返す(InnerBlocks を使う場合は例外)
動的ブロックでは HTML を PHP 側で作るため、JavaScript の save は null を返します。これにより、投稿本文にはブロックコメントと属性だけが保存されます。registerBlockType に save を渡さなかった場合も既定の実装が null を返しますが、意図が伝わるように明示しておくのがおすすめです。
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
registerBlockType( metadata.name, {
edit: Edit,
// 描画は PHP 側で行うので null を返す
save: () => null,
} );
ただし InnerBlocks を使って中に他のブロックを入れ子にする場合は別です。子ブロックのマークアップは投稿本文に保存する必要があるため、save で InnerBlocks.Content を返します。こうして保存された子ブロックの HTML が、render.php の $content に渡ってきます。
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
export default function save() {
const blockProps = useBlockProps.save();
// 子ブロックの HTML だけを保存する
return (
<div { ...blockProps }>
<InnerBlocks.Content />
</div>
);
}
この場合、render.php では $content をそのまま出力して、その周りに PHP で作った要素を足す、という書き方になります。$content はすでに描画済みの HTML なので、esc_html() を通すとタグが文字として表示されてしまう点に注意してください。
register_block_type の render_callback で描画する
render プロパティが追加される前から使われてきた方法が、register_block_type() の第2引数に render_callback を渡すやり方です。こちらはテンプレートファイルではなく PHP の関数で描画します。render.php が「出力する」のに対し、render_callback は HTML を文字列で「返す」点が大きな違いです。echo してしまうと、意図しない位置に出力されるので気をつけてください。
<?php
function webool_register_blocks() {
register_block_type(
__DIR__ . '/build/recent-posts',
array(
'render_callback' => 'webool_render_recent_posts',
)
);
}
add_action( 'init', 'webool_register_blocks' );
/**
* ブロックの HTML を「返す」関数
*
* @param array $attributes ブロックの属性
* @param string $content 内側の HTML
* @param WP_Block $block ブロックのインスタンス
* @return string
*/
function webool_render_recent_posts( $attributes, $content, $block ) {
$number = isset( $attributes['numberOfPosts'] ) ? (int) $attributes['numberOfPosts'] : 5;
return sprintf(
'<p %1$s>最新の投稿を %2$s 件表示します。</p>',
get_block_wrapper_attributes(),
esc_html( $number )
);
}
受け取れる情報は render.php と同じで、順に属性・内側の HTML・WP_Block オブジェクトです。どちらを使うかは好みですが、HTML の量が多いテンプレート寄りの出力なら render.php、条件分岐が多く関数として切り出したい処理なら render_callback が書きやすいでしょう。両方を指定した場合は register_block_type() に渡した render_callback が優先されるため、混在させずどちらか一方に揃えるのが安全です。
出力する値のエスケープを忘れない
動的ブロックの HTML は PHP が組み立てるため、エスケープの責任も自分で負うことになります。属性の値は編集者が入力したものであり、投稿タイトルやユーザー名なども含めて、そのまま echo すると HTML として解釈されてしまいます。出力する場所に合わせて関数を使い分けてください。
| 関数 | 使う場所 |
|---|---|
esc_html() | タグの中に置くテキスト(見出し・本文など) |
esc_attr() | title や alt など属性値 |
esc_url() | href や src の URL |
wp_kses_post() | リンクや強調などのタグを許可したい HTML |
RichText で入力した装飾つきのテキストのように、太字やリンクを残したい値は esc_html() ではタグが文字列になってしまいます。その場合は投稿本文で許可されているタグだけを通す wp_kses_post() を使います。逆に、$content と get_block_wrapper_attributes() の戻り値は WordPress 側で処理済みの HTML なので、エスケープせずにそのまま出力します。
ブロックがフロントに表示されないときの確認点
編集画面では見えているのに、公開ページでは何も出ない。動的ブロックで一番多いトラブルです。原因はだいたい次の3つに絞られます。
render のパスとファイルの場所が合っていない
render のパスは block.json からの相対パスです。register_block_type() には build ディレクトリを渡しているのに、render.php は src にしか無い、という状態だと PHP ファイルが見つからず、ブロックは何も出力しません。ビルド後の build/recent-posts/ の中に block.json と render.php が並んでいるかを確認してください。wp-scripts のコピーは block.json の render の記述を見て行われるため、あとから render を書き足したときはビルドをやり直す必要があります。
save を null に変える前の投稿が残っている
静的ブロックとして作ったものを途中から動的ブロックに変えると、save の出力と投稿本文に保存済みの HTML が食い違い、編集画面で「このブロックには、想定されていないか無効なコンテンツが含まれています」という検証エラーが出ます。開発中であればブロックを一度削除して置き直すのが手早い解決策です。すでに公開済みの投稿がある場合は、古い save の実装を deprecated として登録しておくと、既存のブロックを自動で新しい形へ移行できます。
属性が render.php に届いていない
動的ブロックの属性は投稿本文のブロックコメントに JSON として保存され、そこから $attributes が組み立てられます。そのため、block.json の attributes に定義していないキーは PHP 側に渡ってきません。また、静的ブロックでよく使う source 付きの属性(保存済み HTML から値を読み取る指定)は、保存する HTML が無い動的ブロックでは機能しません。動的ブロックの属性は source を指定せず、型と default だけを書くのが基本です。
編集画面でも同じ見た目を確認したいとき
動的ブロックは PHP が描画するので、edit.js の内容は編集画面専用のプレビューにすぎません。フロントと同じ HTML をエディター上でも見たい場合は、@wordpress/server-side-render パッケージの ServerSideRender コンポーネントを使うと、REST API 経由でサーバー側の描画結果を取得して表示できます。属性を変えるたびに通信が発生するので、入力のたびに更新される項目が多いブロックでは、エディター用の簡易プレビューを自分で書いたほうが快適な場合もあります。
動的ブロックが向いているケース
表示のたびに内容が変わるもの、つまり最新投稿の一覧、関連記事、人気ランキング、閲覧者によって出し分けたい内容などは動的ブロックの得意分野です。WordPress 標準の「最新の投稿」ブロックやクエリーループブロックも動的ブロックとして実装されています。また、内容自体は変わらなくても、あとからマークアップを直したいブロックは動的にしておくと運用が楽になります。既存の投稿を編集し直さなくても、render.php を書き換えるだけですべてのページに反映されるからです。
一方で、見出しやボタンのように内容が固定で、ページを表示するたびに計算する意味がないものは静的ブロックのままで構いません。動的ブロックはリクエストごとに PHP が動くため、データベースへの問い合わせを伴う処理を多用すると表示速度に影響します。必要なところだけ動的にする、という切り分けが現実的です。
まとめ
動的ブロックは、HTML を投稿本文に保存せず、表示のたびに PHP で組み立てるカスタムブロックです。WordPress 6.1 以降は block.json の render に file:./render.php と書くだけでテンプレートを紐付けられ、その中では $attributes・$content・$block が使えます。外側のタグには get_block_wrapper_attributes() を出力して、色や余白などの supports の設定を反映させましょう。JavaScript 側の save は null を返し、InnerBlocks を使うときだけ InnerBlocks.Content を返します。表示されないときは、render.php がビルド先に存在するか、保存済みブロックとの検証エラーが出ていないかを順に確認してみてください。