WordPress・CMS設計

Gutenbergブロックがデータベースに保存され描画される内部構造

seoTitle: Gutenbergブロックの保存と描画を解剖する…

Gutenbergブロックがデータベースに保存され描画される内部構造

metaDescription: Gutenbergブロックがwp_postsに保存され、パーサーとPHPで描画されるまでの仕組みを静的・動的の違いから解説します。

Gutenbergブロックがデータベースに保存され描画される内部構造

カスタムブロックを本番運用に載せると、ほぼ必ず同じ疑問にぶつかる。ブロックの「中身」はデータベースのどこに、どんな形式で保存されているのか。「保存」を押したあと、WordPressの内部では何が起きているのか。そこを押さえずに開発を続けると、プラグイン停止後の表示、バックアップからの復元、テーマ変更時のレイアウト崩れ、ブロック更新時の検証エラーで詰まりやすい。

Gutenbergの扱いで厄介なのは、編集画面で見える構造と、データベースに保存される形式に大きな差があることだ。エディター上では「段落」「画像」「カラム」「カスタムコンポーネント」がブロックの配列として並んでいる。しかし、保存された投稿をデータベースから直接見ると、そこにあるのはブロック専用の配列ではない。HTMLコメントで区切られた、ひと続きのテキストだ。

このテキストを、WordPressは必要なタイミングで再びブロックの構造へ戻す。さらに、ブロックの種類によっては、その構造を使ってサーバー側でHTMLを生成する。つまり、Gutenbergの保存と描画を理解するには、編集画面だけでなく、post_content、ブロックデリミター、パーサー、そしてサーバーサイドレンダリングまで一続きで見る必要がある。

wp_posts テーブルに保存される「ブロック」というデータ形式

まず、最大の前提から確認しておきたい。Gutenbergのブロックは、通常、ブロック専用のテーブルに保存されるわけではない。

WordPressのブロックエディターで作成した投稿本文は、クラシックエディターの本文と同じく、wp_posts テーブルの post_content カラムに格納される。テーブル構造がブロック専用に変わったのではなく、同じカラムに保存される文字列の構造が変わった、と考える方が正確だ。

この違いを見落とすと、データ移行やバックアップ設計で最初から判断を誤る。ブロックの配列が別テーブルに存在するなら、そこだけ移せばよい。しかし実際には、投稿本文の中にブロックの境界と属性が保存されている。投稿を移行するなら、本文の文字列を欠落させずに移さなければならない。

post_content を直接確認すると、投稿編集画面で見えていた整然としたブロック配列は存在しない。たとえば、画像ブロックは次のような形で保存される。

<!-- wp:image {"sizeSlug":"large"} --> でブロックが始まり、その内側に <figure> などのHTMLが続き、<!-- /wp:image --> で閉じる。重要なのは、これが複数のデータを別々に管理しているのではなく、ひとつの本文文字列として保存されている点だ。

保存形式を分解すると、主に次の要素でできている。

  • ブロックの開始位置を示す開始デリミター
  • ブロック名を示す名前空間付きの識別子
  • ブロックの属性を表すJSON
  • 静的ブロックの場合に保存されるHTML
  • ブロックの終端を示す終了デリミター
  • ブロックとして解釈されない通常のHTMLやテキスト

段落ブロックのように、属性がほとんどないブロックでは、開始デリミターが <!-- wp:paragraph --> のようにシンプルになることもある。属性がある場合は、ブロック名の後ろにJSONオブジェクトが続く。

ショートコード時代を知っている開発者ほど、ここで一度混乱する。[gallery ids="1,2,3"] のようなショートコードと、<!-- wp:gallery --> のようなブロックデリミターは、似たように本文へ埋め込まれていても役割が違う。

ショートコードは、本文中の特定の記法を見つけて処理する仕組みだ。一方、ブロックデリミターは、本文の中にブロックの境界と属性を記録するためのシリアライズ形式である。ブロック名、属性、内部HTML、子ブロックの関係を、WordPressのパーサーが読み取れる形で保持する。

wp_postmeta に保存される場合との違い

ブロックの属性がすべて wp_postmeta に保存されるわけではない。通常のブロック属性は、ブロックデリミター内のJSONとして post_content に含まれる。

wp_postmeta が使われるのは、ブロックの実装側が明示的に投稿メタを参照する設計を採用した場合だ。たとえば、本文内には識別子だけを保存し、実際の設定値や外部データを投稿メタ、カスタムテーブル、外部サービスに置く設計は可能である。ただし、それはブロック一般の標準保存形式ではない。

本文とメタを分けると、管理しやすくなるケースもある。大量の構造化データを本文のJSONへ詰め込みたくない場合や、複数の表示箇所から同じデータを参照したい場合だ。しかし、ブロック本文をREST APIやエクスポート機能で扱うときの互換性を優先するなら、まずは標準のブロックデリミター内に属性を保存する設計が扱いやすい。

リビジョンにも注意が必要だ。リビジョンは wp_postspost_type='revision' として保存され、本文のブロックデータもその時点の状態で複製される。投稿を何度も編集すれば、そのたびにブロックを含む本文が履歴として積み上がる。メディアサイトや編集者の多いサイトでは、リビジョンの保持方針がデータベース容量や復元作業のしやすさに影響する。

ブロックの属性をどこへ保存するかは、単なる好みではない。将来の移行、REST APIでの利用、ブロックの再編集、プラグイン停止後の扱いまで関係する。保存形式を決めるときは、編集画面で表示できるかだけでなく、「その本文を別の環境がどう解釈するか」まで見ておきたい。

ブロックデリミターとJSON属性が保持する構造

ブロックデリミター、つまり <!-- wp:ブロック名 --> というHTMLコメントの役割は、単に目印を付けることではない。ブロックの境界と、ブロックが持つ属性を保存するための仕組みだ。

たとえば、次のような段落ブロックを考える。

<!-- wp:paragraph {"align":"right","dropCap":true} -->

ここには、段落ブロックであることに加えて、右寄せとドロップキャップが有効であることが記録されている。属性は文字列や真偽値のような単純な値だけでなく、配列やオブジェクトを持つこともできる。画像のサイズ、リンク設定、レイアウト情報、選択したスタイルなど、ブロックの編集状態を再現するための値が属性として入る。

ただし、デリミターに保存される属性が、そのブロックで利用可能なすべてのデータと一致するとは限らない。属性の定義には、デフォルト値、保存元、型、編集画面での扱いが関係する。値がデリミターに出力されるのか、保存HTMLから取得されるのか、投稿メタなど別の場所から取得されるのかは、ブロックの設計によって決まる。

コメントとして保存する意味

ブロックは、見た目だけを保存しているのではない。HTMLの中に、あとから構造を読み戻せる情報を埋め込んでいる。

HTMLコメントが使われる理由は、本文が基本的にテキストとして扱える状態を保ちながら、WordPress側では構造化データとして解釈できるからだ。ブラウザーが通常のHTMLコメントを画面へ表示しないのと同じように、コメント部分はそのまま見た目へ出ない。本文の中に境界情報を持たせつつ、ブロックを知らない処理系でも文字列として保持しやすい。

ただし、ここから「どの環境でも必ず同じ表示になる」と考えるのは危険だ。コメント自体が残っていても、ブロックの種類や保存方式によって、表示に必要なHTMLが本文内に存在するとは限らない。

静的ブロックなら、デリミターの内側に保存済みのHTMLがある。そのため、ブロックの登録が解除されても、保存されたHTMLが表示に利用される可能性がある。一方、動的ブロックは、本文に属性を持つコメントだけが保存され、表示時にPHPでHTMLを生成する設計が一般的だ。この場合、対応するブロックが登録されていなかったり、サーバー側の描画処理が利用できなかったりすると、保存されたコメントだけでは元の表示内容を再現できないことがある。

つまり、「ブロックプラグインを停止してもテキストと画像はそのまま表示される」とブロック全般について断定することはできない。保存済みHTMLを持つ静的ブロックと、表示時の処理に依存する動的ブロックは、プラグイン停止後の挙動が違う。

インナーブロックはデリミターの入れ子で表現される

カラム、グループ、テンプレートパーツなど、他のブロックを内部に持つブロックは、インナーブロックとして扱われる。保存された post_content では、親ブロックの開始デリミターと終了デリミターの間に、子ブロックのデリミターが入る。

このとき、データベース上では見た目にわかりやすい配列やツリーが保存されているわけではない。あくまで開始・終了デリミターが連続するテキストだ。WordPressはそれをパースして、親子関係を持つブロックの構造へ変換する。エディター側では、この構造が innerBlocks として扱われる。

入れ子が深くなるほど、保存文字列を人間が目視で追うのは難しくなる。特にグループの中にカラムがあり、その中にボタンや画像が入るような構成では、開始デリミターと終了デリミターの対応を手で確認するだけで時間がかかる。本文を直接置換する移行処理を書くなら、単純な文字列置換ではなく、ブロックパーサーを通して構造として扱う方が安全だ。

静的ブロックと動的ブロックのレンダリング

保存形式を理解したら、次は保存されたデータがフロントエンドでどうHTMLになるかを見る。Gutenbergのブロックは、保存と描画の方式から、大きく静的ブロックと動的ブロックに分けて考えられる。

静的ブロックは保存時にHTMLを持つ

静的ブロックでは、エディター側の save() が返したマークアップが、ブロックデリミターの内側に保存される。段落、見出し、画像、単純なボタンのように、保存時点でHTMLを確定できるブロックが代表的だ。

フロントエンドでは、保存済みのHTMLがブロックの内容として利用される。毎回、ブロック専用のPHP処理でデータを取得してHTMLを組み立てる必要はない。その分、表示処理は比較的軽く、ページキャッシュとの相性もよい。

ただし、静的ブロックは「保存したHTMLがその時点の結果である」という特徴を持つ。あとから save() の出力を変更すると、過去の投稿に保存されているHTMLと、現在のブロックが期待するHTMLに差が出る。エディターでその投稿を開いたとき、ブロックのバリデーションエラーとして検出されることがある。

これは、フロントエンドの表示が必ず即座に壊れるという意味ではない。すでに post_content に保存されているHTMLが残っていれば、表示側ではそのHTMLが使われる場合がある。しかし、編集画面での再編集、再保存、変換操作を行うと、古い形式と新しい形式の差が問題になりやすい。

静的ブロックの仕様を変更するなら、属性の追加だけで済むのか、保存HTMLの構造まで変わるのかを切り分ける必要がある。構造を変更する場合は、古いバージョンを deprecated として扱い、過去の保存形式を新しい形式へ変換できるようにする設計が現実的だ。

動的ブロックは表示時にHTMLを生成する

動的ブロックでは、本文に保存されるのはブロック名と属性を含むコメントだけ、という形になることが多い。表示用のHTMLは、ページリクエスト時にサーバー側で生成される。

WordPressは本文をパースしてブロックを特定し、そのブロックに対応する登録情報を探す。登録時に render_callback が指定されていれば、属性やブロックの内容をもとにPHPの処理が実行される。最新記事、ランキング、ユーザーの権限に応じた表示、在庫や外部APIの状態に応じた表示など、保存時に結果を確定できないものは動的ブロックに向いている。

動的ブロックの保存データが少ないことは、必ずしも運用が軽いことを意味しない。表示時にデータベースクエリや外部通信が発生するなら、その処理がページのレスポンスタイムへ影響する。属性として保存されているのは「何を表示するか」の条件であって、「表示結果」そのものではないからだ。

動的ブロックでは、次のような点をあらかじめ設計しておく必要がある。

  • ブロックが参照する投稿やメタ情報の取得方法
  • 表示件数や並び順など、属性値の妥当性検証
  • ログイン状態や権限による表示分岐
  • クエリ結果が空の場合の出力
  • 外部サービスが応答しない場合のフォールバック
  • ページキャッシュやフラグメントキャッシュとの関係
  • ブロック登録が解除された場合に残る保存データの扱い

特に最後の点は、静的ブロックとの違いが表れやすい。動的ブロックは、コメントに対応するサーバー側の登録と描画処理があって初めて、元のHTMLを生成できる。プラグインを停止したり、ブロック登録を削除したりすると、保存されたコメントが残っていても、表示内容まで残るとは限らない。

静的・動的・未登録を並べて考える

観点静的ブロック動的ブロック未登録のブロック
本文に保存されるものデリミターと保存済みHTMLデリミターと属性、必要に応じた内部HTML既存のデリミターと本文内の内容
HTML生成の主な時点保存時表示時登録情報がないため、保存内容やWordPressの処理に依存
render_callback通常は必須ではない主要な描画処理になる利用できない
save() 変更の影響過去の保存HTMLとの不一致が起こりうる表示時のPHP変更が結果に反映されやすい新しい描画ロジックは実行されない
プラグイン停止後保存HTMLが残っていれば表示される可能性がある元の表示を再現できないことがある表示結果を一律には予測できない
主な用途固定的な本文やレイアウト最新データ、条件分岐、外部連携移行途中や登録解除後の保存データ

「未登録」と「不正なマークアップ」も分けて考えたい。未登録とは、保存本文にブロックの記法はあるものの、現在のWordPressへ対応するブロックタイプが登録されていない状態だ。プラグイン停止、テーマ変更、ブロック名の変更、登録処理の失敗などで起こる。

これに対して不正なマークアップは、デリミターの閉じ方が合わない、属性JSONを解釈できない、HTML構造が想定と違うなど、保存文字列そのものが期待する形式から外れている状態を指す。未登録だからといって、必ず不正というわけではない。逆に、登録済みのブロックでも、保存されたHTMLが現在の仕様と一致しなければ、エディターで検証エラーになることがある。

WP_Block_Parser による構文解析

wp_posts.post_content に保存されているのは、HTMLコメントとJSONを含む文字列だ。WordPressがそれをブロックとして処理するには、まず文字列からブロックの境界、属性、内部HTML、子ブロックを取り出す必要がある。そこで使われるのが WP_Block_Parser である。

parse_blocks() を通じて本文を解析すると、ブロックごとの情報が配列として返される。そこには、ブロック名、属性、内部HTML、子ブロック、解析できなかった本文などが含まれる。エディターでブロックを扱う場合も、サーバー側で本文を描画する場合も、保存文字列を構造として読み直す段階が入る。

この処理は、単純に開始タグを見つけて次の終了タグまで切り出すだけではない。インナーブロックがあるため、親子関係を追跡しなければならない。親ブロックの内部に子ブロックが連続している場合、パーサーは現在どの階層にいるかを管理しながら、開始デリミターと終了デリミターを処理する。

再帰下降に近い考え方で階層を読む

ブロックの構文解析を理解するうえでは、「いま何階層のブロックを読んでいるか」を追跡していると考えるとわかりやすい。

1. 本文の中からブロック開始デリミターを探す

2. ブロック名と属性JSONを読み取る

3. 次のデリミターが子ブロックの開始か、現在のブロックの終了かを確認する

4. 子ブロックなら、その子の内容を現在のブロックへ追加する

5. 終了デリミターに到達したら、現在のブロックを親の階層へ戻す

6. 最上位まで戻ったブロックを、解析結果として配列へ格納する

概念としては再帰下降パーサーに近い。しかし、実際のコア実装を単純な再帰関数として説明すると、実装の細部を誤解しやすい。WordPressのパーサーは、状態を持ちながら本文を走査し、ブロックの階層を管理する構成になっている。深い入れ子や長い本文を扱うため、実装上のメモリや呼び出しの扱いにも配慮されている。

PHP側の実装は wp-includes/class-wp-block-parser.php にあり、ブロックのシリアライズ形式を解析する責務を担う。エディター側にもブロックのシリアライズとパースを扱う仕組みがあり、サーバーとJavaScriptで保存形式を共有している。両者が同じデリミターと属性形式を前提にするからこそ、編集画面で保存した本文をサーバー側が読み取れる。

パーサーはブロックの存在を保証するものではない

ここで注意したいのは、パーサーがブロックを「正常化」してくれるわけではないことだ。パーサーの仕事は、保存された文字列を決められた形式に従って解析することだ。ブロックの登録状態や、属性値の意味、保存HTMLが現在の save() と一致するかどうかまで、自動的に保証するものではない。

また、不正なマークアップに対する挙動を「壊れた部分だけを削除し、前後の正常なブロックを必ず活かす」と一般化することもできない。

デリミターの対応関係が崩れている場合、パーサーは本文を可能な範囲で解析しようとするが、どこをブロックとして認識するかは、壊れ方や位置によって変わる。ブロックとして扱われず、通常のHTMLやテキストに近い形で残る部分が出ることもある。反対に、想定していた親子関係とは違う構造として解釈されることもある。

属性JSONが壊れていれば、属性を正しく復元できない。終了デリミターが欠落していれば、後続の本文まで同じブロックの内側として扱われる可能性がある。HTML自体が不正なら、パーサーの結果とブラウザーが最終的に補正して表示する結果が一致するとも限らない。

したがって、次のような説明は避けるべきだ。

  • パーサーは壊れた範囲を必ず無視する
  • 前後のブロックは常にそのまま表示される
  • 一部のブロックが壊れてもサイト全体は落ちない
  • 不正なマークアップでも安全に部分復旧できる

実際のサイトで問題が起きた場合は、parse_blocks() の結果、ブロックの登録状態、保存された属性、テーマやプラグインのフィルター、PHPエラーを個別に確認する必要がある。パーサーには一定のエラー耐性があるとしても、それをサイト全体の停止回避や表示復旧の保証として扱うのは危険だ。

パーサーは壊れた本文を魔法のように修復する機能ではない。保存文字列を構造として読み取るための入口であり、解析結果が正しい表示になるかは登録状態と描画処理にも左右される。

未登録ブロックと不正ブロックの表示は別問題

未登録ブロックの場合、保存本文に開始・終了デリミターと内部HTMLが残っているかどうかで、表示の見え方が変わる。

静的ブロックのように内部HTMLが保存されているなら、現在のブロック登録がなくても、その保存済みHTMLが利用される可能性がある。ただし、テーマやフィルターによる処理、HTMLの状態、WordPressのバージョンなどが関係するため、すべての環境で同一結果になると決めつけることはできない。

動的ブロックのように、本文に実質的な表示HTMLがなく、コメントと属性だけが保存されている場合は、登録情報がない状態で元の見た目を再現できないことがある。これは「未登録ブロックだから壊れた」というより、表示に必要なHTMLを保存時点で持っていない設計だからだ。

不正なマークアップでは、さらに結果を予測しにくい。エディターがブロックを無効な状態として示す場合もあれば、本文の一部が通常のHTMLとして扱われる場合もある。フロントエンドで何が出力されるかは、パース後の配列と各ブロックの描画処理を確認しなければならない。

render_callback が担うサーバーサイドレンダリング

動的ブロックの中心にあるのが、PHPの render_callback だ。register_block_type() でブロックを登録するときにコールバックを指定すると、WordPressはそのブロックを描画する場面でPHP関数を呼び出す。

典型的な流れは、次のようになる。

1. フロントエンドへのページリクエストを受け取る

2. 投稿本文の post_content をブロックとして解析する

3. ブロック名と属性をもとに、登録済みのブロックタイプを探す

4. 動的ブロックであれば render_callback を呼び出す

5. コールバックが返したHTMLを、本文内のブロック位置へ組み込む

6. フィルターなどを経て、最終的な本文として出力する

たとえば、本文に「最新記事を5件表示する」というブロックが保存されている場合、データベースには記事タイトルの一覧そのものではなく、件数や対象条件などの属性が保存される。render_callback はリクエスト時点の投稿データをクエリし、その結果からHTMLを生成する。

この仕組みのおかげで、記事が追加されたときに本文をすべて再保存しなくても、最新記事一覧の表示を更新できる。反面、表示のたびにクエリが走る設計なら、静的ブロックよりサーバー負荷が高くなる。

コールバック内で起きること

render_callback は、単にHTML文字列を返すだけの場所ではない。実際には、属性の検証、データ取得、権限確認、エスケープ、空状態の処理まで担当することになる。

実装時には、少なくとも次のような点を分けて考えたい。

  • 属性が未指定の場合のデフォルト値
  • 数値や識別子を受け取る場合の型と範囲
  • WP_Query などのクエリ条件
  • 権限のないユーザーへ表示してよいデータか
  • 投稿タイトルやURLを適切にエスケープしているか
  • 結果が0件のときに何を返すか
  • 外部APIが失敗したときにページ全体へ影響させない方法
  • 出力するラッパー要素やクラス名を安定させる方法

動的ブロックのPHP処理で例外や致命的エラーが発生すれば、そのリクエストの描画へ影響する。パーサーが本文を読める状態でも、コールバック側の実装が安全とは限らない。表示処理を小さく保ち、取得処理とHTML生成を分けるだけでも、原因の切り分けはかなり楽になる。

クエリとキャッシュはセットで設計する

動的ブロックを複数配置したページでは、各ブロックが個別にクエリを発行することがある。同じ条件のクエリを繰り返せば、表示内容は同じでも処理だけが増える。

まずは、ブロック単位で不要なクエリを発行していないかを見る。次に、更新頻度が低いデータなら、Transient APIなどを利用して結果を一定時間保持する方法を検討する。ユーザーごとに表示が変わるブロックなら、ページ全体のキャッシュと衝突しないよう、キャッシュ単位を細かくする必要がある。

動的ブロックの出力をキャッシュしやすくするには、同じ属性と同じ閲覧条件に対して、予測可能な結果を返す設計が重要だ。コールバックの中で別の本文を再帰的に処理したり、短絡的に do_shortcode() を何度も呼び出したりすると、処理の追跡が難しくなり、キャッシュの境界も曖昧になる。

「動的だから遅い」と決めつける必要はない。クエリ対象を絞り、取得するフィールドを整理し、キャッシュの有効範囲を決めれば、動的ブロックでも十分に運用できる。ただし、静的ブロックのように保存済みHTMLだけを返す場合と比べ、表示時の処理が増えることは前提にしておくべきだ。

block.json とPHPテンプレート

現在のブロック開発では、block.json を中心にブロックを登録する構成も一般的になっている。動的ブロックでは、PHPのコールバックを直接登録する方法だけでなく、render フィールドからPHPテンプレートを指定する方法もある。

登録方法が変わっても、表示時にサーバー側の処理でHTMLを生成するという本質は変わらない。テンプレートへ渡される属性やブロックのコンテキストを確認し、出力値をエスケープし、条件に応じたHTMLを返す必要がある。

新規のカスタムブロックでは、ブロックのメタ情報、エディター用スクリプト、スタイル、サーバー側の描画ファイルを分けて管理できるため、block.json ベースの構成は見通しがよい。一方で、既存プラグインの登録方式を無理に置き換える必要はない。重要なのは登録方法の新旧より、保存形式と描画責務をチーム内で把握できていることだ。

保存形式を理解すると、障害の切り分けが速くなる

Gutenbergの不具合は、編集画面だけを見ていると「ブロックが壊れた」という一言でまとめられてしまう。しかし、実際には複数の層に分かれている。

まず確認するのは、post_content に何が保存されているかだ。開始・終了デリミターがあるのか、属性JSONは読み取れる形か、静的HTMLが内部に残っているのか、動的ブロックのコメントだけなのか。ここで、保存データの問題と描画処理の問題を切り分けられる。

次に、現在の環境でブロックが登録されているかを見る。プラグインが停止していないか、ブロック名が変更されていないか、登録処理が実行されているか。静的ブロックなら保存HTMLの有無が重要になり、動的ブロックなら render_callback やテンプレートが利用できるかが重要になる。

そのうえで、エディターのバリデーションエラーを確認する。静的ブロックでは、現在の save() が返すHTMLと、本文に保存されているHTMLの差が問題になりやすい。動的ブロックでは、保存時の検証で動的な出力を完全には確定できないため、登録情報や属性の扱いが中心になる。

運用時に確認する順番は、次のようにするとよい。

1. wp_posts.post_content の保存文字列を確認する

2. ブロックデリミターの開始・終了と属性JSONを確認する

3. parse_blocks() の解析結果を確認する

4. ブロックタイプが現在の環境で登録されているか確認する

5. 静的HTMLが保存されているか、動的描画に依存しているかを確認する

6. render_callback やPHPテンプレートのエラーを確認する

7. テーマ、フィルター、キャッシュが出力を変更していないか確認する

この順番なら、いきなりエディターの表示だけを直そうとせず、保存、解析、登録、描画のどこで問題が起きているのかを追える。

バックアップと移行で注意すること

ブロック本文が post_content に保存される以上、移行時には通常の投稿本文と同じように扱う必要がある。ただし、ブロックデリミターを含む文字列を、単純な正規表現やHTML変換で加工すると、属性JSONや入れ子構造を壊す可能性がある。

ドメイン置換、画像URLの変更、ブロック名の移行などを行う場合は、変換対象を限定することが重要だ。JSON内の文字列だけを変えるつもりでも、本文HTML内の同じ文字列まで置換されることがある。反対に、HTMLだけを書き換えた結果、属性側の参照先が古いまま残ることもある。

ブロックを構造として変更するなら、パースした結果をもとに対象ブロックを選び、属性や内部ブロックを更新してから再シリアライズする方が安全だ。少なくとも、本番データへ直接置換をかける前に、リビジョンやステージング環境で保存・再編集・表示まで確認したい。

プラグインを置き換える場合も同じだ。新しいブロック名へ移行するなら、旧ブロックの保存形式をどう扱うかを決める必要がある。登録を突然削除すると、静的ブロックは保存HTMLが残る可能性がある一方、動的ブロックは表示に必要な処理を失うことがある。旧ブロックを一定期間登録したまま移行処理を提供する方が、既存投稿への影響を抑えやすい。

Gutenbergの「保存」と「描画」は別の責務である

Gutenbergの内部構造を追うと、編集画面でひとつに見えているブロックが、実際には複数の段階を通っていることがわかる。

投稿本文に保存されるのは、ブロックのデリミター、JSON属性、そして静的ブロックなら保存済みHTMLだ。その文字列を WP_Block_Parser が読み取り、ブロック名や親子関係を持つ構造へ変換する。登録済みのブロックであれば、その情報を使って静的HTMLを利用したり、動的ブロックの render_callback を呼び出したりする。

ここで大切なのは、「ブロックがデータベースに保存されている」という言い方を雑に使わないことだ。保存されているのは、エディター上のオブジェクトそのものではない。あとからブロックへ復元できるようにシリアライズされた本文文字列である。

静的ブロックは、保存時に生成されたHTMLを持つ。そのため、描画時のサーバー処理を減らしやすいが、保存形式の変更には過去投稿の再検証が付いて回る。動的ブロックは、表示時に最新の状態を反映できる。その代わり、登録とサーバー側の描画処理が失われると、コメントだけでは元の表示を再現できない場合がある。

未登録ブロックや不正なマークアップについても、結果を一律に保証することはできない。解析できる範囲が残るケースはあるが、壊れた部分だけが安全に取り除かれ、他の部分が必ず正常表示されるとは限らない。表示の確認には、保存文字列、パース結果、登録状態、描画処理を個別に見る必要がある。

この構造を知っていると、カスタムブロックを設計するときの判断が変わる。保存すべきものは何か、表示時に取得すべきものは何か、プラグイン停止後に最低限残したいHTMLは何か。これらを先に決められるからだ。

Gutenbergのブロックは、編集画面の部品ではない。本文に埋め込まれた構造化データであり、パーサーによって読み戻され、登録されたブロックの仕様に応じて描画される。wp_posts の文字列から始まり、デリミター、JSON、WP_Block_Parserrender_callback へつながる流れを押さえることが、ブロックを長く運用するための最初の設計になる。

Related reading: WordPressとヘッドレスCMSの分岐点:個人開発で選ぶべき設計と移行の実態.

よくある質問

Gutenbergブロックのデータはデータベースのどこに保存されますか?
ブロック専用のテーブルではなく、投稿本文と同じwp_postsテーブルのpost_contentカラムに、HTMLコメントで区切られた文字列として保存されます。
静的ブロックと動的ブロックの保存形式の違いは何ですか?
静的ブロックはデリミター内にHTMLを保持しますが、動的ブロックは主にブロック名と属性を含むコメントのみを保存し、表示時にサーバー側でHTMLを生成します。
プラグインを停止するとブロックの表示はどうなりますか?
静的ブロックは保存済みのHTMLが残っていれば表示される可能性がありますが、動的ブロックは表示に必要なサーバー側の処理が失われるため、元の表示を再現できないことがあります。
ブロックの属性はどこに保存されますか?
基本的にはブロックデリミター内のJSONとしてpost_contentに含まれますが、設計によってはwp_postmetaやカスタムテーブルに保存することも可能です。
WP_Block_Parserは壊れたブロックを自動的に修復してくれますか?
いいえ、パーサーは保存文字列を解析するだけであり、ブロックの登録状態や属性の整合性、HTMLの破損を自動的に修復する機能はありません。

参考情報