投稿の中にHTMLをダイナミックに挿入するショートコードをいくつか作っています。たとえば [ lf_bookinfo asin=XXX]
というショートコードは ASIN(Amazon.co.jp における書籍ID)をパラメータとし、書影や書籍情報、関連するリストなどを出力します。
これまではショートコードに登録したコールバックの中で素朴にHTMLを書いていました。しかしブロックエディタには「縦積み」「横並び」のようなデザインブロックがあって、複雑なレイアウトを簡単に作れます。
この恩恵に与りたいと思い、ブロックエディタが生成したコードをショートコードのコールバック内にコピーしたりしてみました。が、ブロックエディタが生成する複雑なクラスなどに追随するのが大変で断念。新しく専用ブロックを作成して既存の投稿を全置換するのが正道。しかしブロックの作成もまた面倒でした。
試行錯誤の末、表示したいHTMLを再利用ブロックとして登録し、ショートコードの中から呼び出す方法に落ち着きました。
表示レイアウトをデザインして再利用ブロックとして保存
表示させたい情報をブロックエディタでレイアウトし、ダミーのテキストや画像を挿入しておきます。
下部の見出しとリストはデータがあれば表示し、なかったら表示しないのですが、とりあえず表示させる前提でデザインしておきます。これを再利用ブロックとして保存すると、 <!-- wp:block {"ref":NNN} /-->
のようなコードが挿入されます。
ショートコード内から再利用ブロックを呼び出す
WordPress はブロックを解析(パース)して表示用のHTMLを描画(レンダー)し、その後でショートコードをコールバックの出力で置き換えます。ショートコードが処理される時点では、本体のブロックパース&レンダリングは終わってしまっています。よって再利用ブロックのパースとレンダリングは手動で行わねばなりません。
再利用ブロックは 投稿タイプ wp_block の投稿なので、それを呼び出してブロックにパースします。パースも標準関数があるので簡単です。
// 再利用ブロックを取得
$q = new WP_Query( [
'post_type' => 'wp_block',
'p' => NNN,
] );
// HTMLをブロックにパースする
$parsed_blocks = parse_blocks( $q->posts[0]->post_content );
ブロックの内容を置き換える
内容を置き換えるブロックには、再利用ブロックを作るときに固有のCSSクラス名を振っておきます。意味合い的にはIDがふさわしいところですが、IDはブロックの属性情報としては保持されていない(出力HTMLにのみ書き込まれている)ので扱いづらい。
次のようなイメージで、ブロックの内容を置き換えていきます。
// 特定のクラスのブロックの内容を置き換える
block_replace( $parsed_blocks[0], [
'class' => 'lf-attribute-as-id',
'from' => '<li>(attribute)</li>',
'to' => '<li>1行目</li><li>2行目</li>',
] );
ブロックの内容を置き換える処理はこんな感じ。ブロックおよびブロック内ブロックを再帰的に探索し、クラス名が一致したブロックの innerContentを置き換えます。パースして得られたブロックを直接置き換えていくために参照渡しにしています。
function block_replace( &$block, $args ) {
// 渡されたブロックを直接書き換えるのでリファレンス渡し
foreach ( $block as &$b ) {
// 枝葉
if( ! is_array( $b ) ) {
continue;
}
// 置換対象のブロックなら
$cn = $b['attrs']['className'] ?? '';
if ( $args['class'] === $cn ) {
// このブロックの innerContent を置き換える
$b['innerContent'][0] = str_replace(
$args['from'],
$args['to'],
$b['innerContent'][0]
);
break;
}
// 直下に innerBlocks か添字配列があれば再帰呼び出し
if ( array_key_exists( 'innerBlocks', $b )
|| array_key_exists( 0, $b ) ) {
block_replace( $b, $args );
}
}
}
再帰ってすばらしい。
array_walk_recursive() でも
置き換える元となる文字列(この例では ‘<li>(attribute)</li>’)を一意にしておけば、array_walk_recursive() で置換する方法もあります。クラス指定も要らないし楽かも。
array_walk_recursive( $parsed_blocks, 'block_replace', [
'from' => [
'<li>(attribute)</li>',
'(more-heading)',
],
'to' => [
'<li>1行目</li><li>2行目</li>',
$more_heading_to,
],
] );
function block_replace( &$item, $key, $args ) {
$item_replaced = str_replace( $args['from'], $args['to'], $item, $count );
if ( 0 !== $count ) {
$item = ( '' !== $args['to'] ) ? $item_replaced : '';
}
}
array_walk_recursive() のコールバック関数の中で、置換先の文字列が空のときにブロック要素を丸ごと消している($item に ’’ を代入している)のは、<h>や<ul>などの外側のHTMLを含めてすべて削除するためです。
HTML出力
そのような処理を置き換えたいブロックについて実施した後、HTMLにレンダーしてコールバック終了。
return render_block( $parsed_blocks[0] );
少々面倒ではありましたが、既存の投稿を変更しなくてよい点、ブロックエディタでデザインできる点、そして再利用ブロックなので変更を一斉に及ぼせる点はメリットです。