- GutenbergのQuoteブロックに追加クラスを付けると、フロント側だけ吹き出しUIへ変換するWordPressプラグインを作りました。
- 保存データはWordPress標準の構造のままなので、プラグインをオフにすれば元の引用ブロックに戻ります。
- Cocoonのような固有ブロックと違い、テーマを乗り換えてもコンテンツが壊れません。
- HTML解析にDOMDocumentを使い、変換できない場合は元のHTMLをそのまま返す設計で、表示が崩れないようにしています。
1. 設計の出発点
Gutenbergの標準Quoteブロックに追加CSSクラスを付けると、フロント表示だけ吹き出しUIへ変換するWordPressプラグインを作りました。

以前はCocoonの吹き出しブロックを使っていました。
使いやすく見た目も良いのですが、テーマ固有のブロックなので、別テーマへ移行しようとすると独自ブロックがそのまま残ります。

Cocoonを外した瞬間にコンテンツが壊れるのが困りどころでした1。
そこで今回は「保存するデータはWordPress標準の構造のまま、表示だけプラグイン側で担う」という役割分担にしました2。
1.1. 動作の仕組み
Quoteブロックはすでに引用文と出典を持つ構造なので、吹き出しのデータモデルとして十分使えます。
追加クラスを目印にして変換し、クラスがなければ普通のQuoteとして表示します。
プラグインをオフにすれば元の引用ブロックに戻るので、テーマを乗り換えてもコンテンツの構造は残ります。
フックの登録はメインPHPの1行だけです。
add_filter( 'render_block_core/quote', 'chiilabo_qsb_render_quote_block', 10, 3 );Code language: JavaScript (javascript)
エディタでは普通のQuoteブロックとして書きます。


<blockquote class="wp-block-quote robot">
<p>こんな感じ</p>
<cite>Robot</cite>
</blockquote>Code language: HTML, XML (xml)
render_block_core/quote フィルタがフロント表示のタイミングだけ動き、こう展開します3。
<div class="chiilabo-speech-balloon robot">
<div class="chiilabo-speech-person">
<img src="https://example.com/robot.png" class="chiilabo-speech-icon" alt="">
<div class="chiilabo-speech-name">Robot</div>
</div>
<div class="chiilabo-speech-body">
<p>本当に役に立つアプリなら、押し付けなくても使われるのに……</p>
</div>
</div>Code language: JavaScript (javascript)
クラス名と画像URLの対応は管理画面の「設定」→「Quote Speech Balloon」で設定します。

複数クラスが付いている場合は左から最初に一致したものを採用し、cite を話者名、それ以外を本文として使います4。
注意点としては、追加CSSクラスを設定するのは、引用ブロックに入れることです。
つい、うっかり引用内の段落ブロックに入れてしまうと、反映されません。
2. 実装で気を使ったところ
プラグインのファイル構成、
chiilabo-quote-speech-balloon/
├─ chiilabo-quote-speech-balloon.php
├─ includes/
│ ├─ renderer.php # 変換ロジック
│ └─ settings.php # 管理画面
└─ assets/
└─ css/
└─ speech-balloon.cssCode language: PHP (php)
2.1. HTML解析にDOMDocumentを使う
Quoteブロックは p や ul などInnerBlocksを含む可能性があり、構造が固定されていません5。
正規表現を避けて DOMDocument でノードを走査することで、中身の構造が変わっても cite だけ取り出して残りを本文として扱えます。
blockquote の子ノードをループして、cite タグのものは話者名として取り出し、それ以外を本文に連結しています。
foreach ( $blockquote->childNodes as $child ) {
if ( XML_ELEMENT_NODE === $child->nodeType && 'cite' === strtolower( $child->nodeName ) ) {
if ( '' === $speaker ) {
$speaker = trim( wp_strip_all_tags( $child->textContent ) );
}
continue;
}
$body_html .= $dom->saveHTML( $child );
}Code language: PHP (php)
2.2. 変換できないときは元のHTMLを返す
DOMの読み込み失敗、マッピング未設定、本文が空など、条件を満たさなければ元の blockquote をそのまま返します。
プラグインをオフにしたときと表示が変わらないので、何が起きても表示が壊れません6。
関数の冒頭でガード節を並べ、どの段階でも失敗したら即 return $block_content で元のHTMLを返す構造になっています。
if ( ! is_string( $block_content ) || '' === trim( $block_content ) ) {
return $block_content;
}
if ( false === stripos( $block_content, '<blockquote' ) ) {
return $block_content;
}
$class_to_image = chiilabo_qsb_get_class_to_image_map();
if ( empty( $class_to_image ) ) {
return $block_content;
}
// DOM解析失敗時もここで return $block_contentCode language: PHP (php)
2.3. サニタイズを二段階でかける
設定保存時に sanitize_html_class() と esc_url_raw() をかけ、レンダリング時にも esc_url()・esc_html()・wp_kses_post() を通します。
設定画面経由でない値が混入した場合も、出力側で除去します7。
保存時のサニタイズはこうなっています。
// 設定保存時(settings.php)
$class_name = sanitize_html_class( (string) $classes[ $i ] );
$image_url = esc_url_raw( (string) $urls[ $i ] );Code language: PHP (php)
レンダリング時の出力エスケープはこちらです。
// 出力時(renderer.php)
sprintf(
'<div class="chiilabo-speech-balloon %1$s">...<img src="%2$s" ...>%3$s...%4$s...</div>',
esc_attr( sanitize_html_class( $matched_class ) ),
esc_url( $image_url ),
esc_html( $speaker ),
wp_kses_post( $body_html )
);Code language: PHP (php)- WordPressのテーマ固有ブロックやショートコードが投稿データに残り続ける問題は「テーマロックイン」と呼ばれ、WordPressコミュニティでも長く議論されている。 – WordPress Theme Lock-In, Silos, and the Block System – WP Tavern
- Gutenbergの静的ブロックは
save関数の出力とデータベース保存値を照合し、差異があるとBlock Validation Errorを出す。フロントの表示変更をサーバーサイドのrender_blockフィルタで行えば、保存データを変えずに済むためこの問題を回避できる。 – Block Filters – Block Editor Handbook | Developer.WordPress.org render_block_{ブロック名}は特定ブロックのフロントHTMLだけを対象にできる動的フィルタで、WordPress 5.7で追加された。汎用のrender_blockフィルタと異なり、対象外のブロックのコールバック呼び出しが発生しない。 – Block Filters – Block Editor Handbook | Developer.WordPress.org- WordPressのQuoteブロックは内部的に
blockquote+citeのセマンティックなHTMLを生成する。cite要素を使った引用者の明示はアクセシビリティ上も推奨されている。 – Accessible Blockquotes: Coding Guide – The Admin Bar - Quoteブロックの内部には段落ブロックだけでなく他のブロックも挿入できる。WordPress.comの公式ドキュメントにも「Quote内にも他のブロックを追加できる」と記載されている。 – Quote block – WordPress.com Support
- WordPressのBlock Filters公式ドキュメントでも、保存済み投稿コンテンツを変更したい場合は
saveフィルタではなくサーバーサイドのrender_blockを使うことを明示的に推奨している。 – Block Filters – Block Editor Handbook | Developer.WordPress.org wp_kses_post()はwp_kses($data, 'post')のラッパーで、WordPressの投稿コンテンツに許可されているタグと属性だけを残してそれ以外を除去する。 – wp_kses_post() – Function | Developer.WordPress.org