1. ホーム
  2. WordPress

【WordPress】render_block フィルターの使い方|ブロックの出力HTMLをPHPで書き換える方法

Share

「コアの画像ブロックに独自のクラスを足したい」「テーブルブロックを横スクロールできる div で包みたい」——ブロックエディターを使っていると、既存ブロックの出力 HTML をほんの少しだけ変えたくなる場面があります。JavaScript でブロックの save を書き換えると既存記事がブロック検証エラーになってしまいますが、PHP の render_block フィルターならその心配がありません。この記事では render_block の基本的な使い方から、特定のブロックだけを対象にする方法、WP_HTML_Tag_Processor を使った安全な書き換えまでを解説します。

render_block はブロックの出力を通す最後の関門

投稿本文に保存されているのは、ブロックコメント付きの HTML です。これをフロントで表示するとき、WordPress は本文をブロック単位に解析し、1つずつ HTML を組み立てて連結します。render_block は、そのブロック1つ分の HTML ができあがった直後に呼ばれるフィルターです。ここで文字列を返し直せば、そのままページに出力されます。

重要なのは、これがフロントエンドの出力だけに効くことです。投稿本文として保存されている HTML には手を触れないので、ブロックエディターを開いたときの検証には影響しません。save を変更したときのように deprecated を書いて過去の記事を救済する、といった作業も不要です。あとからフィルターを外せば、出力は元どおりに戻ります。

渡ってくる引数は3つです。

引数型内容
$block_contentstringそのブロックが出力する HTML。これを加工して返す
$blockarray解析済みのブロック情報。blockName と attrs が入っている
$instanceWP_Blockブロックのインスタンス。context などを参照できる

基本の使い方

render_block はページ内のすべてのブロックに対して呼ばれます。段落にも見出しにも画像にも、1つずつ順番に呼ばれるため、まず対象のブロックかどうかを $block['blockName'] で判定するのが定石です。

functions.php
function webool_wrap_table_block( $block_content, $block ) {
    // テーブルブロック以外はそのまま返す
    if ( 'core/table' !== $block['blockName'] ) {
        return $block_content;
    }

    // 横スクロールできる div で包む
    return '<div class="table-scroll">' . $block_content . '</div>';
}
add_filter( 'render_block', 'webool_wrap_table_block', 10, 2 );

add_filter() の第4引数に 2 を渡している点に注意してください。既定では第1引数しか渡らないので、$block を受け取るには受け取る引数の数を明示する必要があります。

そして対象外のときは必ず $block_content をそのまま返すこと。return; と書いたり、うっかり null を返したりすると、そのブロックの中身がページから丸ごと消えます。フィルターは「値を受け取って値を返す」関数なので、加工しない場合も返却が必要です。

ブロック名のわかりやすい調べ方

コアブロックの名前は core/paragraph、core/heading、core/image、core/list のように core/ で始まります。正確な名前がわからないときは、投稿の本文に埋め込まれているブロックコメント(<!-- wp:image --> など)を見るのが手軽です。ここの wp: の後ろが名前で、core/ は省略されて保存されます。つまり <!-- wp:image --> のブロック名は core/image、<!-- wp:webool-blocks/code-wrapper --> なら webool-blocks/code-wrapper です。

特定のブロックだけを対象にする render_block_{name}

WordPress 5.7 以降には、ブロック名ごとの動的フィルター render_block_{$block_name} が用意されています。フック名にブロック名をそのまま含める形で、名前の判定を自分で書かずに済みます。

functions.php
function webool_wrap_table_block( $block_content ) {
    return '<div class="table-scroll">' . $block_content . '</div>';
}
// フック名に core/table をそのまま書く(スラッシュもそのまま)
add_filter( 'render_block_core/table', 'webool_wrap_table_block' );

判定が消えるぶん読みやすく、対象ブロック以外では関数自体が呼ばれないので無駄もありません。1種類のブロックだけを狙うならこちらを使ってください。逆に「複数のブロックに同じ処理をかけたい」「ブロック名の一部で判定したい」ときは、素の render_block のほうが書きやすくなります。

WP_HTML_Tag_Processor で属性を安全に書き換える

「包む」だけなら文字列の連結で済みますが、既存のタグにクラスや属性を足すとなると話が変わります。str_replace() や正規表現で class=" を探す書き方は、属性の順番が違ったり、本文中の別のテキストに当たったりして簡単に壊れます。

WordPress 6.2 以降では WP_HTML_Tag_Processor が使えます。HTML を正しく解釈しながらタグを探し、属性を読み書きできるクラスです。

functions.php
function webool_add_class_to_image( $block_content ) {
    $processor = new WP_HTML_Tag_Processor( $block_content );

    // 最初の img タグまで進む
    if ( $processor->next_tag( 'img' ) ) {
        // クラスを追加する(既存のクラスは保たれる)
        $processor->add_class( 'is-zoomable' );

        // 属性を足す・上書きする
        $processor->set_attribute( 'decoding', 'async' );
    }

    // 加工結果を文字列として取り出す
    return $processor->get_updated_html();
}
add_filter( 'render_block_core/image', 'webool_add_class_to_image' );

よく使うメソッドは次のとおりです。

メソッド働き
next_tag( $query )条件に合う次のタグへ進む。見つかれば true
get_attribute( $name )現在のタグの属性値を取得する
set_attribute( $name, $value )属性を設定する(無ければ追加、あれば上書き)
remove_attribute( $name )属性を削除する
add_class( $class )class 属性にクラスを追加する
remove_class( $class )class 属性からクラスを削除する
get_updated_html()変更を反映した HTML を返す

next_tag() には配列で条件を渡せます。$processor->next_tag( array( 'tag_name' => 'a', 'class_name' => 'wp-block-button__link' ) ) のように書くと、特定のクラスを持つ a 要素だけを探せます。

複数のタグを順に処理する

next_tag() は呼ぶたびに次のタグへ進むので、while で回せばブロック内のすべてのタグを処理できます。外部リンクにだけ rel を足す例を見てみます。

functions.php
function webool_mark_external_links( $block_content, $block ) {
    if ( 'core/paragraph' !== $block['blockName'] && 'core/list' !== $block['blockName'] ) {
        return $block_content;
    }

    $processor = new WP_HTML_Tag_Processor( $block_content );
    $home      = wp_parse_url( home_url(), PHP_URL_HOST );

    while ( $processor->next_tag( 'a' ) ) {
        $href = $processor->get_attribute( 'href' );
        if ( ! $href ) {
            continue;
        }

        $host = wp_parse_url( $href, PHP_URL_HOST );

        // 自サイト以外へのリンクだけに印を付ける
        if ( $host && $host !== $home ) {
            $processor->add_class( 'is-external' );
            $processor->set_attribute( 'rel', 'noopener' );
        }
    }

    return $processor->get_updated_html();
}
add_filter( 'render_block', 'webool_mark_external_links', 10, 2 );

正規表現なら組み立てに苦労するうえ、href の中に > が含まれるようなケースで壊れかねません。WP_HTML_Tag_Processor は HTML の構造を理解して読み進めるので、その種の破綻が起きません。

ブロックの属性や文脈で分岐する

第2引数の $block['attrs'] には、ブロックコメントに保存されている属性が配列で入っています。<!-- wp:heading {"level":3} --> なら array( 'level' => 3 ) です。これを見れば、同じブロックでも設定によって処理を分けられます。

functions.php
function webool_add_anchor_to_h2( $block_content, $block ) {
    // level は h2 のとき保存されないことがあるので既定値を 2 にする
    $level = isset( $block['attrs']['level'] ) ? (int) $block['attrs']['level'] : 2;

    if ( 2 !== $level ) {
        return $block_content;
    }

    $processor = new WP_HTML_Tag_Processor( $block_content );
    if ( $processor->next_tag( 'h2' ) ) {
        $processor->add_class( 'has-anchor' );
    }

    return $processor->get_updated_html();
}
add_filter( 'render_block_core/heading', 'webool_add_anchor_to_h2', 10, 2 );

ここで気をつけたいのは、既定値の属性はブロックコメントに保存されないことです。見出しブロックの level は既定が 2 なので、h2 の場合 attrs に level は入りません。isset() で確認して既定値を補う書き方が必要になります。

表示している場所で出し分ける

render_block は投稿本文だけでなく、テンプレートパーツやウィジェット、抜粋の生成中などブロックがレンダリングされるあらゆる場面で呼ばれます。「単一記事ページの本文だけ」に限定したいときは、条件分岐タグを併用します。

functions.php
function webool_wrap_table_block( $block_content ) {
    // 管理画面・フィード・一覧ページでは何もしない
    if ( is_admin() || is_feed() || ! is_singular() ) {
        return $block_content;
    }

    return '<div class="table-scroll">' . $block_content . '</div>';
}
add_filter( 'render_block_core/table', 'webool_wrap_table_block' );

関連するフィルターとの使い分け

ブロックのレンダリング周りには似た名前のフィルターがいくつかあり、呼ばれるタイミングが違います。

フィルタータイミング向いている用途
render_block_dataレンダリング前。解析済みの配列を受け取る属性や innerHTML を書き換える、既定値を差し込む
render_block_contextレンダリング前。ブロックに渡る文脈を受け取るpostId など子ブロックに渡る値を差し替える
render_blockレンダリング後。出力 HTML を受け取るクラス・属性の追加、要素での包み込み

「出てきた HTML をいじりたい」なら render_block、「そもそもブロックに渡る値を変えたい」なら render_block_data です。たとえば画像ブロックの sizeSlug を一律で変えたい場合は、出力後の src を書き換えるより render_block_data で属性を直したほうが素直に済みます。

フィルターが効かないとき

エディターの見た目は変わらない

これは仕様です。render_block は PHP 側でフロントの HTML を組み立てるときのフィルターなので、JavaScript で描画されるブロックエディターの表示には影響しません。編集画面の見た目も変えたいなら、editor.BlockListBlock フィルターや editorStyle の CSS を別途用意することになります。エディターとフロントで見え方を揃えたい場合は、この点が render_block の弱点です。

入れ子のブロックで二重に適用される

カラムブロックやグループブロックのような入れ子構造では、子ブロックに対して呼ばれたあと、親ブロックに対しても呼ばれます。親の $block_content には子の出力が含まれているため、「a タグを全部書き換える」といった処理を素の render_block で書くと、同じリンクが二度処理されることになります。

属性の set_attribute() や add_class() は上書き・重複回避が効くので実害が出にくいのですが、文字列を追記するような処理では結果が二重になります。対象を core/paragraph のような末端のブロックに絞るか、すでに処理済みかどうかを属性で判定して回避してください。

クラシックブロックやショートコードには効かない

ブロックエディター以前の記事は、本文全体が1つのクラシックブロック(blockName が null)として扱われます。個々の段落や画像は独立したブロックではないので、core/image を狙ったフィルターは当たりません。古い記事も対象にしたい場合は、the_content フィルター側で処理する必要があります。

まとめ

render_block は、ブロック1つ分の出力 HTML ができた直後に呼ばれるフィルターです。投稿本文そのものには手を入れないため、ブロック検証エラーを心配せずに既存ブロックの出力を調整できます。すべてのブロックで呼ばれるので $block['blockName'] での判定は必須ですが、1種類だけが対象なら render_block_core/table のような動的フィルターを使うほうが簡潔です。

属性やクラスを足す処理は、str_replace() や正規表現ではなく WP_HTML_Tag_Processor を使ってください。next_tag() で目的のタグまで進み、add_class() や set_attribute() で変更し、get_updated_html() で取り出す。この流れを覚えておけば、HTML の書き方に左右されない安全な書き換えができます。

注意点は、エディターの表示には効かないこと、入れ子のブロックでは親子それぞれに呼ばれること、そしてクラシックブロックには当たらないことの3つです。まずは「テーブルを横スクロール可能にする」あたりの小さな用途から試して、フィルターが通るタイミングの感覚をつかんでみてください。

参考ページ