Claude CodeでGutenbergブロック検証エラーをdeprecatedで直す
save()を書き換えた後に既存投稿へ出る「予期しないコンテンツ」を、deprecated配列とmigrateで救う手順です。古い保存形式を固定し、Claude Codeに検証させる進め方も示します。
ブロックのsave()を書き換えると、書き換え前に保存した投稿でブロックが無効と判定されることがあります。原因は、保存済みのマークアップと、新しいsave()が作り直すマークアップが食い違うことです。これを救うのがdeprecated配列で、古いsave()を残しておけば、既存投稿は新しい形へ移行されます。
Claude Codeに任せると、save()だけを直してdeprecatedを足し忘れる流れになりがちです。ここでは、古い保存形式を先に固定し、deprecatedを足し、固定した内容で検証させる順に進めます。ブロックの新規作成はClaude CodeでGutenbergブロックを作るで扱っているので、ここでは既存ブロックの保存形式を変える場面に絞ります。
検証エラーはなぜ起きるのか
エディターは起動時に、投稿内のすべてのブロックを検証します。保存済みのマークアップから属性を読み取り、その属性でsave()をもう一度実行して、保存済みの内容と一致するかを比べます。一致しなければ、そのブロックは無効です。
つまり、save()の出力を変えた時点で、変更前に保存したブロックはすべて不一致になります。利用者が何も触っていなくても、エディターで開いた瞬間に警告が出ます。
保存済みの内容は、投稿に次のような形で入っています。
<!-- wp:my-plugin/notice {"text":"メンテナンスのお知らせ"} -->
<p class="wp-block-my-plugin-notice">メンテナンスのお知らせ</p>
<!-- /wp:my-plugin/notice -->カスタムブロックはwp:の後に名前空間と名前が続き、属性はコメント内のJSONに入ります。この保存形式はデータベースに残り続けるので、コードだけを書き換えても過去の投稿は変わりません。
エラーを見つけたときの手がかりは、ブラウザーの開発者ツールのコンソールです。ブロックが無効と判定されると警告が出力され、マークアップが食い違った箇所が示されます。期待されたマークアップと実際のマークアップの差を、まずここで読みます。
対処は2つの方針から選ぶ
保存形式を変えたときの選択肢は、大きく分けて次の2つです。
- ブロックに
deprecatedを足さず、別の名前の新しいブロックを作る - 古い形を
deprecatedとして残し、新しいブロックで編集できるようにする
通常は後者を選びます。なお、すでに無効になったブロックを利用者が手で直す方法もあります。無効なブロックには「Attempt Block Recovery」のボタンが出ます。三点メニューからは「HTMLに変換」「クラシックブロックに変換」なども選べます。ただし、これらは投稿を1件ずつ直す作業です。プラグインの作者がコード側でdeprecatedを用意すれば、すべての投稿が一度に救われます。
古い保存形式を先に固定する
deprecatedには、古いsave()の実装が正確に必要です。記憶や推測で書くと、古い形と一致せず、救えません。そこで、save()を書き換える前に、保存形式を取り出して残します。
- 書き換え前の
save()とattributesを、gitの履歴から確認する - 書き換え前のコードでブロックを置いた投稿を、フィクスチャーとして保存する
- 書き換え後に、同じフィクスチャーで検証する
ブロックの各バージョンのコンテンツをフィクスチャーとして残しておくと、新しいdeprecatedが過去のすべてのバージョンで動くかを検証しやすくなります。
Claude Codeには、この固定までを先に頼みます。CLAUDE.mdに次の約束を足しておくと、毎回の指示が短くなります。
## ブロックの保存形式を変えるとき
- `save()`・`attributes`・`supports`のいずれかを変える前に、変更前の内容を`deprecated`用の定数(v1、v2…)として残す
- `deprecated`配列は新しいバージョンから順に並べる
- `deprecated`内の`save()`から別ファイルの関数をimportしない。必要なら複製を同じファイルに置く
- 変更前の保存形式のマークアップを`fixtures/`に置き、書き換え後に同じ内容で検証する最後から2行目は、importの落とし穴に対応します。deprecatedのsave()が他のファイルの関数を読み込んでいると、そのファイルの変更で古い形の出力が意図せず変わることがあります。その時点の関数を複製してdeprecatedのファイルに置けば、古い出力は固定されます。
deprecatedを足す: タグだけを変えた場合
最も単純なのは、<p>を<div>に変えただけの変更です。公式の例に沿うと、次の形になります。
const v1 = {
attributes: {
text: { type: 'string', source: 'html', selector: 'p' },
},
save( { attributes } ) {
return <p>{ attributes.text }</p>;
},
};
registerBlockType( 'my-plugin/notice', {
attributes: {
text: { type: 'string', source: 'html', selector: 'div' },
},
save( { attributes } ) {
return <div>{ attributes.text }</div>;
},
deprecated: [ v1 ],
} );ここで落としやすいのが、attributesとsupportsの扱いです。これらは現行のブロックから自動で引き継がれません。解析や保存の結果に影響するので、deprecatedの側に、そのバージョンのものを書く必要があります。
属性のselectorも変わっています。古い形ではp、新しい形ではdivです。保存形式が変わると、属性をどこから読み取るかも変わります。属性の定義まで含めて古い形を残してください。
属性名を変えた場合はmigrateで橋渡しする
属性の名前を変えたときは、古い属性を新しい属性へ移すmigrateが必要です。textをcontentに変える例です。
const v1 = {
attributes: {
text: { type: 'string', source: 'html', selector: 'p' },
},
migrate( { text } ) {
return { content: text };
},
save( { attributes } ) {
return <p>{ attributes.text }</p>;
},
};migrateは古い属性とインナーブロックを受け取り、新しい属性を返す関数です。属性とインナーブロックの組を配列で返すこともできます。後者は、たとえばtitle属性を段落のインナーブロックに移すような変更に使います。
isEligibleは、ブロックが有効でも移行したい場合の関数です。技術的には有効でも、属性やインナーブロックの更新が必要なときに使います。
deprecatedが要らない無効ブロックもある
無効なブロックがすべてsave()の変更で起きるわけではありません。無効になる代表的な原因は2つあります。1つは、ブロックのコードの欠陥が意図しない内容の変更を生むこと。もう1つは、利用者や外部のエディターがブロックのHTMLを書き換え、正しいとみなされない形になったことです。
save()を触っていないのに警告が出るなら、deprecatedを足す前にこちらを疑います。切り分けの手がかりは次の2点です。
- 属性の読み取り:
source付きの属性は、マークアップから期待どおりに読み取られ、正しい型(多くは文字列か数値)で保存されているかを確認します - 外部の情報への依存:
save()が外部の情報を出力に含めていると、その情報が変わった後に編集した投稿が無効になることがあります。保存する出力には、外部の情報を含めない設計にします
Claude Codeに原因調査を頼むときは、コンソールの警告を丸ごと貼ります。警告には、期待されたマークアップと実際のマークアップの差が出ているので、save()の変更が原因か、属性の読み取りの不具合かを切り分ける材料になります。差の出どころがsave()の最近の変更なら、git logで変更前の実装を取り出し、deprecatedに回します。
deprecatedが連鎖しないことを知っておく
つまずきやすいのは、複数のdeprecatedがデータベースのマイグレーションのように連鎖しないことです。実際の動きは次のとおりです。
deprecatedが選ばれる流れ
- 1
現行のsaveで検証する
現行の
save()が有効なマークアップを作れなければ、deprecated配列の先頭が試されます。 - 2
最初に有効になった定義を採用する
その
save()が有効な結果を返せば、その定義で属性を読み取り、migrateがあれば実行します。無効なら次の定義へ進みます。 - 3
現行のsaveで書き直す
採用された定義から得た属性が現行の
save()に渡され、新しい形で保存されます。ここで流れは終わります。
この仕組みから、2つの注意が出てきます。1つ目は、save()が無効な結果を返したdeprecatedは、migrateごと完全に飛ばされることです。isEligibleがtrueを返す場合でも同じです。
2つ目は、新しい移行を足すとき、複数のdeprecatedのmigrateを直す必要が出ることです。たとえば内容をインナーブロックへ移す変更を足すなら、v1とv2の両方のmigrateに反映しないと、古い投稿だけ移行されません。
順序は新しいバージョンから古いバージョンへ並べます。最新で可能性の高い定義から試すことで、無駄で高コストな処理を避けられます。
Claude Codeに検証させる
実装が済んだら、検証をClaude Codeに回させます。Claude Codeには、エラーと再現するコマンドを伝えるのが基本の頼み方です。ここでは、再現する手順が「古い投稿をエディターで開く」ことなので、その手順を渡す形にします。
save()をv1からv2に変えた。fixtures/notice-v1.htmlの内容は変更前に保存した投稿。
ブロックがdeprecatedで移行されることを確認して。
まず現行コードでfixturesを投入し、無効になる状態を再現してから直して。フィクスチャーの投入は、wp post createにファイルを渡す形で行えます。wp-env runの基本はClaude CodeでGutenbergブロックを作るにあります。ここでの要点は、投入する内容が固定したフィクスチャーである点です。ファイルのパスはコンテナ内から見たパスで書くので、<plugin>は自分のプラグインのディレクトリ名に置き換えます。
npx wp-env run cli "wp post create --post_type=page --post_status=publish \
--post_title='deprecated確認' \
wp-content/plugins/<plugin>/fixtures/notice-v1.html"エディターの警告は画面とコンソールに出るので、確認は人の目で行います。Claude Codeにはnpm run buildとnpm run lint:js(@wordpress/scriptsを使うプロジェクトを前提にしています)を通すところまでを頼み、エディターで開いた結果は人が報告する流れにすると、役割が混ざりません。
テストを書く場合は、既存のテストを確認させたうえでテストを追加させ、「新しいテストを実行して失敗を直す」よう頼む流れが使えます。Claude Codeは既存のテストの書き方に合わせてテストを作ります。ブロックのテストの仕組みがあるプロジェクトなら、各バージョンのフィクスチャーに対するテストを足してもらいます。
よくあるつまずき
- 古い
save()を推測で書いてしまう: gitの履歴から、変更前の実装を正確に取り出して渡す attributesをdeprecatedに書き忘れる: 現行から引き継がれないので、古い定義を丸ごと写すdeprecatedの順序が逆: 新しいバージョンを先頭にするmigrateが一部の定義にしかない: 無効と判定された定義はmigrateごと飛ばされるので、必要な定義すべてに書く- 投稿を直接書き換えて済ませる: 利用者が開いたときに移行される仕組みなので、投稿データを手で直す必要はない
まとめ
save()を変えるときの手順は、変更前の形を残し、deprecatedを足し、固定した内容で検証する3段階です。Claude Codeに頼むなら、最初の1段階を省かせないことが分かれ目になります。古い形が正確に残っていれば、あとはdeprecatedの仕組みが移行を引き受けます。
編集のたびに検査を走らせたいときは、phpcsのhookのようにhookへ寄せる方法があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。