PHP でレンダリングする動的ブロックを作ると、エディター側は空っぽのまま「ここに最新記事が入ります」といった説明を置くだけ、ということになりがちです。ServerSideRender を使うと、PHP が実際に出力する HTML をそのままエディター内に表示できます。この記事では @wordpress/server-side-render パッケージの基本的な使い方、属性を渡してプレビューを更新する方法、主要な props、そしてプレビューが真っ白になるときの原因を解説します。WordPress 6 系のブロックエディターと @wordpress/scripts によるビルド環境を前提としています。
目次
動的ブロックのプレビューが必要になる理由
動的ブロックは save が null を返し、表示のたびに render.php が HTML を組み立てます。つまり投稿本文には <!-- wp:webool/recent-posts --> のようなコメントしか保存されておらず、エディターは表示すべき HTML を何も持っていません。最新記事一覧やアンケート結果のように内容がその都度変わるブロックでは、これは仕組み上どうしようもない制約です。
そこで ServerSideRender は、エディターから REST API の /wp/v2/block-renderer/<ブロック名> エンドポイントを呼び出し、サーバー側で render.php を実行した結果の HTML を受け取って画面に差し込みます。編集者はフロントとほぼ同じ見た目を確認しながら設定を調整できます。edit の中で JavaScript による見た目の再現を書き直す必要もありません。
一方で、表示のたびに通信が発生する、返ってきた HTML はクリックできない静的な塊になる、といった性質もあります。編集操作が必要なテキストは RichText で編集して属性に保存し、ServerSideRender は「サーバーでしか作れない部分」に使う、という切り分けが現実的です。
基本の使い方
ServerSideRender は @wordpress/server-side-render パッケージのデフォルトエクスポートです。block にブロック名を渡すだけで動きます。
import { useBlockProps } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
export default function Edit() {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<ServerSideRender block="webool/recent-posts" />
</div>
);
}
block に渡す名前は block.json の name と完全に一致させます。名前空間(webool/ の部分)を忘れると、あとで説明する「ブロックが見つからない」エラーになります。
ServerSideRender を useBlockProps() を付けた要素の中に置いている点にも注目してください。ServerSideRender 自身はラッパー用の div を出力しますが、ブロックのルート要素としては扱われません。useBlockProps() は必ず自前の要素に展開します。
PHP 側とビルド設定
サーバー側は通常の動的ブロックと同じで、block.json に render を書き、render.php で HTML を返します。特別な追加設定はありません。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "webool/recent-posts",
"title": "最新記事一覧",
"category": "widgets",
"attributes": {
"numberOfPosts": {
"type": "number",
"default": 3
}
},
"editorScript": "file:./index.js",
"render": "file:./render.php"
}
<?php
// $attributes に block.json で定義した属性が入ってくる
$posts = get_posts( array(
'numberposts' => $attributes['numberOfPosts'],
) );
if ( empty( $posts ) ) {
return; // 空を返すと ServerSideRender は「空です」の表示になる
}
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
<?php foreach ( $posts as $post ) : ?>
<li>
<a href="<?php echo esc_url( get_permalink( $post ) ); ?>">
<?php echo esc_html( get_the_title( $post ) ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
JavaScript 側では wp-server-side-render というスクリプトハンドルへの依存が必要ですが、@wordpress/scripts でビルドしていれば import 文から自動的に検出され、index.asset.php の依存配列に追加されます。手書きで wp_enqueue_script() している場合だけ、依存配列に 'wp-server-side-render' を自分で足してください。
属性を渡してプレビューを更新する
attributes props にブロックの属性オブジェクトを渡すと、その値がリクエストのクエリに乗り、render.php の $attributes に届きます。渡した属性が変わるたびに再取得が走るので、サイドバーで件数を変えた瞬間にプレビューも切り替わります。
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl } from '@wordpress/components';
import ServerSideRender from '@wordpress/server-side-render';
import { __ } from '@wordpress/i18n';
export default function Edit( { attributes, setAttributes } ) {
const { numberOfPosts } = attributes;
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={ __( '表示設定', 'webool' ) }>
<RangeControl
label={ __( '表示件数', 'webool' ) }
value={ numberOfPosts }
onChange={ ( value ) =>
setAttributes( { numberOfPosts: value } )
}
min={ 1 }
max={ 10 }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<ServerSideRender
block="webool/recent-posts"
attributes={ attributes }
/>
</div>
</>
);
}
渡すのは attributes オブジェクトそのままで構いません。ただし block.json の attributes に定義していないキーを混ぜると、REST API 側の検証ではじかれて 400 エラーになります。定義済みの属性だけを渡すのが安全です。
また、この書き方だとスライダーを動かしている間ずっとリクエストが飛びます。件数程度なら気になりませんが、テキスト入力に連動させる場合は @wordpress/compose の useDebounce で値を間引いてから渡すと、通信回数をかなり減らせます。
import { useState, useEffect } from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';
// attributes をそのまま渡さず、500ms 遅れて追従する値を作る
const [ preview, setPreview ] = useState( attributes );
const updatePreview = useDebounce( setPreview, 500 );
useEffect( () => {
updatePreview( attributes );
}, [ attributes ] );
// <ServerSideRender block="webool/recent-posts" attributes={ preview } />
指定できる props
必須なのは block だけですが、表示のカスタマイズや通信方法の切り替えに使える props が用意されています。
| props | 役割 |
|---|---|
block | レンダリングするブロック名(必須)。block.json の name と一致させる |
attributes | サーバーに渡す属性オブジェクト。変化するたびに再取得する |
urlQueryArgs | リクエスト URL に足すクエリ。プレビュー対象の投稿 ID を渡すときなどに使う |
httpMethod | 'GET'(既定)か 'POST'。属性が多く URL が長くなる場合は 'POST' にする |
skipBlockSupportAttributes | true にすると supports 由来の色・余白などの属性をリクエストから除く |
LoadingResponsePlaceholder | 読み込み中に表示するコンポーネント |
EmptyResponsePlaceholder | レスポンスが空だったときに表示するコンポーネント |
ErrorResponsePlaceholder | エラー時に表示するコンポーネント |
3つの *Placeholder は、既定のそっけないメッセージを自前の文言に差し替えるためのものです。コンポーネントを渡す形なので、Spinner や Placeholder と組み合わせるとエディター全体の見た目に馴染みます。
import { Placeholder, Spinner } from '@wordpress/components';
const Loading = () => (
<Placeholder>
<Spinner />
</Placeholder>
);
const Empty = () => (
<Placeholder>表示できる記事がありません。</Placeholder>
);
<ServerSideRender
block="webool/recent-posts"
attributes={ attributes }
httpMethod="POST"
LoadingResponsePlaceholder={ Loading }
EmptyResponsePlaceholder={ Empty }
/>
httpMethod="POST" は、長いテキスト属性を渡すブロックでは実質必須です。GET のままだと属性が URL のクエリ文字列に載るため、サーバーの URL 長制限に引っかかってリクエストが失敗します。
プレビューが表示されないときに見る場所
エディターに何も出ない、あるいはエラー文言だけが出る場合、原因はだいたい次のいずれかに絞られます。ブラウザーの開発者ツールでネットワークタブを開き、block-renderer へのリクエストのステータスを確認するのが最短の切り分けです。
ブロック名が一致していない
「ブロックが見つかりません」と表示されたときは、block props の文字列と block.json の name を突き合わせます。名前空間の付け忘れや、ハイフンとアンダースコアの取り違えがよくある原因です。レスポンスは 404 になります。
もうひとつ見落としやすいのが、ブロックが PHP 側で登録されていないケースです。block-renderer エンドポイントはサーバー側の登録情報を参照するため、JavaScript の registerBlockType だけでは足りません。register_block_type( __DIR__ . '/build' ) がプラグインの init フックで動いているか確認してください。
属性の検証で 400 エラーになる
REST API は受け取った属性を block.json の定義と照合します。定義にないキーや、type が合わない値(数値のはずが文字列で来ている等)があるとリクエストが弾かれます。RangeControl や SelectControl の onChange が文字列を返していないか、parseInt() を挟むべきところが抜けていないかを見直します。
ブロックに supports で色や余白を持たせている場合、それらの属性も一緒に送られます。これがサーバー側の想定と噛み合わないときは skipBlockSupportAttributes を true にすると回避できます。
render.php が空を返している
ステータスが 200 なのに何も出ないときは、PHP が本当に空文字列を返しています。get_posts() の条件が厳しすぎる、条件分岐で return してしまっている、といった単純な理由がほとんどです。EmptyResponsePlaceholder を指定しておくと「空である」ことがはっきり分かるので、原因の切り分けが早くなります。
なお render.php の中で echo ではなく return で文字列を返す書き方をしている場合、その値は使われません。render ファイルは出力バッファで内容を拾う仕組みなので、HTML は必ず出力してください。
スタイルが当たらず崩れて見える
HTML は出ているのにフロントと見た目が違うときは、CSS がエディターに読み込まれていません。block.json の style に指定した CSS はエディターにも読み込まれますが、editorStyle にしか書いていないもの、テーマ側の CSS に依存しているものは反映されません。共通の見た目は style に置くのが基本です。
まとめ
ServerSideRender は、動的ブロックの「エディターでは中身が見えない」問題を、PHP の出力をそのまま持ってくることで解決するコンポーネントです。block にブロック名、attributes に属性を渡すだけで、設定を変えるたびに最新のプレビューが表示されます。エディター用の見た目を JavaScript で二重に実装せずに済むのが最大の利点です。
実運用では、属性の多いブロックでは httpMethod="POST" を指定すること、入力に連動させるなら useDebounce でリクエストを間引くこと、この2点を押さえておくと安定します。うまく表示されないときは、まず block-renderer へのリクエストが 404 なのか 400 なのか 200 なのかを見る。それだけで原因はほぼ絞り込めます。
参考ページ
- @wordpress/server-side-render | Block Editor Handbook | WordPress Developer Resources
- Creating dynamic blocks | Block Editor Handbook | WordPress Developer Resources
- register_block_type() – Function | WordPress Developer Resources
- get_block_wrapper_attributes() – Function | WordPress Developer Resources