1. ホーム
  2. WordPress

【WordPress】カスタムブロックの deprecated の使い方|save を変更してもブロック検証エラーを出さない方法

Share

自作のカスタムブロックを公開したあとで save のマークアップを変更したところ、これまで問題なく表示できていた投稿を開いた瞬間に「このブロックには、想定されていないか無効なコンテンツが含まれています。」と赤い枠が出た——ブロック開発でほぼ必ず一度は通る場面です。この記事では、なぜこのブロック検証エラー(Block validation failed)が起きるのかという仕組みから、block.json の変更ではなく JavaScript 側の deprecated に古い定義を登録して古い投稿を自動的に移行させる方法までを解説します。attributes の形を変えたときに使う migrate、判定を自分で書く isEligible も扱います。WordPress 6 系、apiVersion 3 を前提としています。

ブロック検証エラーは「保存済みHTML」と「今の save」の照合で起きる

save を持つ静的ブロックは、編集して保存した時点で save が返したマークアップがそのまま投稿本文に文字列として書き込まれます。データベースに入っているのは JSON ではなく、ブロックコメントに挟まれた完成済みの HTML です。

保存されている投稿本文
<!-- wp:webool/notice {"message":"注意してください"} -->
<div class="wp-block-webool-notice notice"><p>注意してください</p></div>
<!-- /wp:webool/notice -->

投稿を開き直すとき、ブロックエディターはこの本文をそのまま画面に流し込むわけではありません。ブロックコメントに書かれた属性(ここでは message)を読み取り、今インストールされているブロックの save を実際に呼び出してマークアップを生成します。そして、生成した結果と本文に書かれている HTML を比較します。この照合を検証(validation)と呼び、一致しなかったときに出るのが「無効なコンテンツ」の警告です。

つまりエラーの原因は投稿データが壊れたことではなく、保存した当時の save と、いま動いている save が食い違っていることです。クラス名を1つ足した、pdiv に変えた、要素の入れ子を1段深くした——どれも同じように検証を失敗させます。

比較は完全一致ではないが、構造の違いは許されない

比較は単純な文字列一致ではなく、HTML として解析したうえで行われます。属性の並び順の違い、真偽値属性の書き方、意味を持たない空白の差といった見た目に影響しない揺れは吸収されます。そのため、コードの整形を変えただけで壊れるようなことはありません。

一方で、要素の種類・入れ子の構造・クラス名や属性の値の違いは、そのまま不一致として扱われます。開発中に何度も save を書き換えるのは当然ですが、その変更がすでに保存された投稿に及ぶという点を意識しておく必要があります。

「リカバリーを試行」で済ませてはいけない理由

エラーが出たブロックには「ブロックのリカバリーを試行」というボタンが表示されます。これを押すと、属性から今の save でマークアップを作り直し、正常なブロックに戻せます。1つ2つなら手っ取り早い方法です。

ただし、これは投稿を開いた人が手作業で押して、保存し直して初めて直る方法です。ブロックを100記事で使っていれば100回押すことになりますし、押さないまま放置された投稿はエディターを開くたびに警告が出続けます。さらに、リカバリーは属性から復元できる情報しか救えないため、属性として保持していない部分(InnerBlocks の中身や、source の指定を変えてしまった属性など)が失われることもあります。

これを開発者側で先回りして解決する仕組みが deprecated です。

deprecated に「昔の save」を登録しておく

deprecated は、registerBlockType に渡す設定のひとつで、過去のバージョンのブロック定義を配列で並べたものです。検証が失敗したとき、エディターは即エラーにする前に、この配列を上から順に試します。どれかの定義で検証が通れば、そのブロックは「古い形式で保存されたもの」として正しく読み込まれ、次に投稿を保存したタイミングで自動的に今の save の形式で書き直されます。編集者は何が起きたか気づく必要すらありません。

実際の例で見ていきます。次のような「お知らせ」ブロックがあり、すでに複数の投稿で使われているとします。

src/save.js(変更前・v1)
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const blockProps = useBlockProps.save( { className: 'notice' } );

    return (
        <div { ...blockProps }>
            <RichText.Content tagName="p" value={ attributes.message } />
        </div>
    );
}

ここで「アイコンを置く場所がほしいので、本文を div で包みたい」という変更が入ったとします。新しい save はこうなります。

src/save.js(変更後・v2)
import { useBlockProps, RichText } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const blockProps = useBlockProps.save( { className: 'notice' } );

    return (
        <div { ...blockProps }>
            <div className="notice__body">
                <RichText.Content tagName="p" value={ attributes.message } />
            </div>
        </div>
    );
}

この状態で古い投稿を開けば検証エラーです。そこで、変更前の save をそのまま deprecated に移します。専用のファイルを作っておくと見通しがよくなります。

src/deprecated.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

// v1: 本文を div で包む前の save
const v1 = {
    attributes: {
        message: {
            type: 'string',
            source: 'html',
            selector: 'p',
        },
    },
    save( { attributes } ) {
        const blockProps = useBlockProps.save( { className: 'notice' } );

        return (
            <div { ...blockProps }>
                <RichText.Content tagName="p" value={ attributes.message } />
            </div>
        );
    },
};

// 新しいものから順に並べる
export default [ v1 ];
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import deprecated from './deprecated';

registerBlockType( metadata.name, {
    edit: Edit,
    save,
    deprecated, // ここに配列を渡す
} );

これでビルドし直すと、古い投稿を開いても警告は出なくなります。エディターは「今の save では一致しないが、v1save となら一致する」と判断し、そのブロックを正常なものとして読み込みます。あとはその投稿を更新すれば、本文は自動的に v2 の形式に書き換わります。

attributes と supports も一緒に書き写す

deprecated の各要素には save だけでなく、そのバージョン当時の attributessupports も書きます。書かなかった項目は block.json の現在の値が使われる仕様ですが、これに頼ると後から block.json を編集したときに古いバージョンの定義まで意図せず変わってしまいます。上の例のように、当時の内容を明示的に書いておくほうが安全です。

特に注意したいのが source を持つ属性です。source: 'html' の属性は selector で指定した要素の中身から値を読み出すため、マークアップを変えると属性の読み取り自体が失敗します。上の例では selector: 'p' のままなので変わりませんが、たとえば pspan に変えたなら、deprecated 側には selector: 'p' を、今の attributes には selector: 'span' を書く必要があります。

deprecated 版の save で useBlockProps を省略しない

useBlockProps.save()wp-block-webool-notice のような自動生成クラスや、supports 由来のクラス・インラインスタイルを出力します。古い save をコピーするときにここを省いてしまうと、当時実際に保存された HTML と一致しなくなり、deprecated を書いたのにエラーが消えないという状態になります。当時のコードをそのまま持ってくるのが鉄則で、思い出しながら書き直そうとしないことが大切です。

deprecated の各要素に書ける項目

配列の1要素に指定できる項目を整理します。必須なのは save だけで、残りは必要に応じて足します。

項目役割
saveそのバージョンのマークアップを返す関数。保存済み HTML との照合に使われる(必須)
attributesそのバージョン当時の属性定義。省略すると現在の block.json の定義が使われる
supportsそのバージョン当時の supports。色や余白の設定を後から足した場合に必要になる
migrate古い属性・子ブロックを新しい形に変換する関数。属性の名前や型を変えたときに使う
isEligibleこの定義を適用すべきかを自分で判定する関数。true を返すと検証が通っていても移行が実行される

配列の順序は新しいバージョンから古いバージョンへ並べるのが推奨です。エディターは先頭から順に試すので、使われている可能性が高い新しめの定義を前に置くほうが無駄な照合が減ります。

属性の形を変えたときは migrate で変換する

マークアップだけでなく属性そのものを変えることもあります。たとえば type という文字列の属性を level という数値に作り替えた、2つの属性を1つのオブジェクトにまとめた、といったケースです。この場合、古い投稿に保存されているのは古い名前の属性なので、save を差し替えるだけでは値が失われます。

そこで使うのが migrate です。古い属性を受け取り、新しい属性のオブジェクトを返す関数を書きます。ここでは、isWarning という真偽値の属性を、level という文字列('info''warning')に置き換える例を示します。

src/deprecated.js(migrate を使う)
import { useBlockProps, RichText } from '@wordpress/block-editor';

const v1 = {
    attributes: {
        message: { type: 'string', source: 'html', selector: 'p' },
        isWarning: { type: 'boolean', default: false },
    },

    // 古い属性 → 新しい属性への変換
    migrate( oldAttributes ) {
        const { isWarning, ...rest } = oldAttributes;

        return {
            ...rest,
            level: isWarning ? 'warning' : 'info',
        };
    },

    save( { attributes } ) {
        const className = attributes.isWarning ? 'notice is-warning' : 'notice';
        const blockProps = useBlockProps.save( { className } );

        return (
            <div { ...blockProps }>
                <RichText.Content tagName="p" value={ attributes.message } />
            </div>
        );
    },
};

export default [ v1 ];

ポイントは、migrate の中で参照するのは古い属性名だという点です。引数に渡ってくるのは v1attributes の定義に従って読み取られた値なので、isWarning がそのまま入っています。返したオブジェクトが新しい属性として使われ、以降は level で動きます。

migrateInnerBlocks を持つブロックでも使えます。その場合は第2引数に子ブロックの配列が渡り、[ 新しい属性, 新しい子ブロックの配列 ] という配列を返すことで子ブロックの構成も差し替えられます。子ブロックまで変える必要がなければ、オブジェクトだけを返せば十分です。

isEligible で「検証は通るが移行したい」場合に対応する

deprecated は基本的に「検証に失敗したとき」に働きます。しかし、まれに検証には通ってしまうが、それでも古い形式として扱いたいケースがあります。たとえばマークアップは変えずに属性だけを変更した場合、古い保存済み HTML でも今の save と一致してしまい、migrate が呼ばれません。

そういうときは isEligible を書きます。この関数が true を返すと、検証結果にかかわらずその定義が適用され、migrate が実行されます。

src/deprecated.js(isEligible を使う)
const v1 = {
    attributes: {
        message: { type: 'string', source: 'html', selector: 'p' },
        isWarning: { type: 'boolean', default: false },
    },

    // 古い属性が残っているブロックだけを対象にする
    isEligible( attributes ) {
        return attributes.isWarning !== undefined;
    },

    migrate( oldAttributes ) {
        const { isWarning, ...rest } = oldAttributes;
        return { ...rest, level: isWarning ? 'warning' : 'info' };
    },

    save( { attributes } ) {
        // v1 当時の save をそのまま
    },
};

判定は「古い形式かどうか」を確実に見分けられる条件にします。上の例のように古い属性が存在するかどうかを見るのが分かりやすい書き方です。常に true を返すような雑な条件にすると、すでに新しい形式になっているブロックまで毎回 migrate にかけてしまうので避けてください。

deprecated を書いてもエラーが消えないとき

deprecated は「一致する定義が1つでもあれば通る」仕組みなので、消えないということはどの定義とも一致していないということです。原因の見つけ方を順に見ていきます。

ブラウザーのコンソールで差分を確認する

検証に失敗すると、開発モードのビルドではブラウザーのコンソールに「Block validation: Block validation failed for …」という警告と一緒に、期待した HTML と実際の HTML の両方が出力されます。どちらの文字列も表示されるので、目で見比べればクラス名が1つ多い、要素が1段深い、といった差はすぐに分かります。

ここで確認すべきなのは、期待側の HTML がどのバージョンの save のものかです。deprecated に登録したはずの形が候補に出てきていないなら、そもそも registerBlockTypedeprecated を渡せていない可能性があります。src/index.js で import と受け渡しができているか見直してください。

ビルド後のファイルが更新されていない

WordPress が読み込むのは src ではなく build の中身です。wp-scripts start を起動したままファイルを追加した場合、新しいファイルが監視対象に入らずビルドされていないことがあります。deprecated.js を新規作成したあとは一度ビルドを止めて起動し直すか、wp-scripts build を実行して build/index.js に反映されているか確認しましょう。ブラウザーのキャッシュが残っている場合もあるので、スーパーリロードも試してください。

途中のバージョンを飛ばしている

save を2回変更したのに deprecated には最初の形だけを登録している、というパターンです。この場合、v2 の時期に保存された投稿はどの定義とも一致しません。公開後に save を変更するたびに、その直前の形を deprecated の先頭に積み増すのが正しい運用です。

src/deprecated.js(複数バージョン)
// 新しいものから古いものへ並べる
export default [ v2, v1 ];

バージョンが増えてくると、それぞれの save がいつの形なのか分からなくなりがちです。v1 / v2 といった変数名に加えて、「本文を div で包む前」のようなコメントを添えておくと、半年後の自分が助かります。

そもそも deprecated を増やしたくない場合

マークアップを頻繁に調整したいブロックなら、動的ブロックにしてしまうのが根本的な対策になります。savenull を返し、block.jsonrender に PHP ファイルを指定すると、フロントの HTML は表示のたびに PHP が生成します。投稿本文に保存されるのはブロックコメントと属性だけになるため、照合すべきマークアップが存在せず、検証エラーが起きません。

ただし、動的ブロックはページ表示のたびに PHP が動くこと、静的な HTML として本文に残らないためブロックを削除するとフロントの表示も消えることなど、性質が変わります。装飾中心のシンプルなブロックは静的のまま deprecated で運用し、出力内容が状況によって変わるブロックは動的にする、といった使い分けが現実的です。

まとめ

ブロック検証エラーは、投稿本文に保存された HTML と、いまの save が生成する HTML が食い違うことで起きます。save を変更した以上は避けられない現象で、「リカバリーを試行」での手作業に頼るとブロックを使っている投稿の数だけ手間がかかります。

対策は、registerBlockTypedeprecated に変更前の定義を登録しておくことです。検証に失敗したときエディターがこの配列を順に試し、一致するものが見つかれば正常に読み込んだうえで、次の保存時に新しい形式へ自動的に書き換えてくれます。各要素には当時の attributessupports も書き写し、save思い出して書き直すのではなく当時のコードをそのままコピーするのが確実です。属性の名前や型まで変えたときは migrate で古い値を新しい形に変換し、検証が通ってしまうケースでは isEligible で対象を判定します。

公開後に save を触るときは deprecated を1つ積む、という手順をセットで覚えておけば、あとから安心してマークアップを改善できます。

参考ページ