自作のカスタムブロックに「画像を1枚選ばせたい」という要件はよく出てきます。入力欄に URL を貼らせるのではなく、WordPress でおなじみのメディアライブラリのモーダルを開いて選んでもらえれば、編集する人の手間もミスも一気に減ります。この記事では @wordpress/block-editor が提供する MediaUpload と MediaUploadCheck、そしてより手軽な MediaPlaceholder を使って、ブロックの編集画面に画像選択の UI を付ける方法を、block.json の属性定義から save.js の出力まで通しで解説します。
目次
MediaUpload はメディアライブラリのモーダルを開くための部品
MediaUpload は、それ自体が見た目を持つコンポーネントではありません。「メディアライブラリのモーダルを開く関数」を用意してくれるだけの部品で、ボタンやリンクといった見た目は自分で書きます。ユーザーがそのボタンを押すとモーダルが開き、画像を選んで「選択」を押した瞬間に、選ばれた画像の情報がコールバックへ渡ってきます。
渡ってくる情報のうち、ブロック側で保存しておきたいのは基本的に画像の ID と URLの2つです。URL は表示にそのまま使え、ID は「どのメディアなのか」を特定するために使います。この2つをブロックの属性(attributes)に入れておけば、あとは save.js で <img> として書き出すだけで完成します。
ここでは my-blocks/figure という名前のブロックを例に進めます。ビルド環境は @wordpress/scripts(wp-scripts start / wp-scripts build)を前提としています。
block.json に画像用の属性を用意する
まずは選んだ画像を覚えておくための入れ物を block.json の attributes に作ります。画像の ID は数値なので number、URL と代替テキストは文字列なので string です。default を指定しておくと、ブロックを挿入した直後の値が決まるので、edit.js 側で「未定義かどうか」を気にせず済みます。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-blocks/figure",
"title": "画像ブロック",
"category": "media",
"icon": "format-image",
"textdomain": "my-blocks",
"editorScript": "file:./index.js",
"attributes": {
"mediaId": {
"type": "number",
"default": 0
},
"mediaUrl": {
"type": "string",
"default": ""
},
"mediaAlt": {
"type": "string",
"default": ""
}
}
}
source を指定していないので、これらの値は保存された HTML から読み取るのではなく、ブロックコメント(<!-- wp:my-blocks/figure {"mediaId":123,...} -->)の中に JSON として保存されます。画像のように「HTML の属性から読み戻すのが面倒なもの」は、この形が一番素直です。
MediaUpload の基本の書き方
MediaUpload の特徴は render プロパティです。ここに関数を渡すと、その関数の引数として { open } というオブジェクトが渡ってきます。open はモーダルを開く関数なので、ボタンの onClick にそのまま指定します。React でいう render props と呼ばれる書き方です。
import { useBlockProps, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const { mediaId } = attributes;
return (
<div { ...useBlockProps() }>
<MediaUploadCheck>
<MediaUpload
// 画像が選ばれたときに呼ばれる
onSelect={ ( media ) =>
setAttributes( {
mediaId: media.id,
mediaUrl: media.url,
mediaAlt: media.alt,
} )
}
allowedTypes={ [ 'image' ] } // 画像だけを選べるようにする
value={ mediaId } // 選択中の画像をモーダルで反転表示させる
render={ ( { open } ) => (
<Button variant="primary" onClick={ open }>
メディアを追加
</Button>
) }
/>
</MediaUploadCheck>
</div>
);
}
これだけで、ブロックの中に「メディアを追加」ボタンが表示され、押すと見慣れたメディアライブラリのモーダルが開きます。画像を選ぶと onSelect が呼ばれ、setAttributes() で属性に保存される、という流れです。
value に現在選択中の ID を渡しておくのがポイントです。これを省いてもモーダルは開きますが、渡しておくと再度開いたときに前回選んだ画像が選択済みの状態になり、「変更」の操作が分かりやすくなります。
MediaUploadCheck で囲む理由
上のコードで MediaUpload を MediaUploadCheck で包んでいるのには理由があります。WordPress のユーザーは全員がファイルをアップロードできるわけではありません。たとえば寄稿者(Contributor)権限のユーザーは upload_files 権限を持っていないため、メディアライブラリへのアップロードができません。
MediaUploadCheck は、現在のユーザーがメディアを追加する権限を持っているかどうかを確認し、権限がある場合だけ中身(children)を描画します。権限がなければ何も表示しません。つまり、囲んでおくだけで「押しても何もできないボタン」を権限のないユーザーに見せずに済みます。
囲まなかった場合、寄稿者にもボタンが表示されてしまいます。押せばモーダル自体は開きますが、アップロードのタブが使えなかったり、意図した操作ができずに混乱を招きます。ブロックの UI としては不親切なので、MediaUpload を使うときは MediaUploadCheck で包むのが基本形だと覚えておくとよいでしょう。
権限がないときに「何も出ない」のが不安であれば、fallback プロパティに代わりの表示を渡せます。
<MediaUploadCheck
fallback={ <p>画像をアップロードする権限がありません。</p> }
>
{ /* MediaUpload をここに置く */ }
</MediaUploadCheck>
MediaUpload に渡せる主なプロパティ
よく使うものを整理すると次のようになります。必須と言えるのは onSelect と render の2つで、残りは用途に応じて足していく形です。
| プロパティ | 内容 |
|---|---|
onSelect | メディアが選択されたときに呼ばれる関数。単一選択ならメディアオブジェクト、複数選択ならその配列が渡される |
render | モーダルを開くための UI を返す関数。引数の { open } がモーダルを開く関数 |
allowedTypes | 選択できるメディアの種類を配列で指定する。[ 'image' ] のような MIME タイプの大分類や、[ 'image/png' ] のような具体的な指定ができる |
value | 現在選択中のメディア ID。複数選択時は ID の配列。モーダルを開いたときの選択状態に反映される |
multiple | true にすると複数のメディアを選択できる。既定は false |
gallery | true にするとギャラリー編集用のモーダルとして開く。既定は false |
addToGallery | ギャラリーモードで、既存の選択に追加する形でモーダルを開く。既定は false |
title | モーダルのタイトル文字列。既定は「メディアを選択またはアップロード」相当の文言 |
allowedTypes を指定しないと、画像だけでなく動画や PDF などライブラリ内のすべてのファイルが選択候補に出てきます。画像を想定したブロックなら [ 'image' ] を明示しておきましょう。
onSelect に渡ってくるメディアオブジェクトの中身
onSelect の引数には、選ばれたメディアの情報がオブジェクトとして渡ってきます。よく使うキーは id(メディアの ID)、url(フルサイズの URL)、alt(代替テキスト)、caption(キャプション)、title(タイトル)、mime(MIME タイプ)あたりです。
画像の場合はこれに加えて sizes が入っています。WordPress はアップロード時に thumbnail / medium / large といったサイズ違いの画像を自動生成しますが、その一覧がここに入ってくる形です。それぞれが url / width / height を持っています。
const onSelectImage = ( media ) => {
// サムネイルがあればそれを、無ければフルサイズの URL を使う
const thumbnailUrl = media.sizes?.thumbnail?.url ?? media.url;
setAttributes( {
mediaId: media.id,
mediaUrl: media.url,
mediaAlt: media.alt ?? '',
thumbnailUrl,
} );
};
注意したいのは、指定したサイズが必ず存在するとは限らないことです。元画像より大きいサイズは生成されないため、小さな画像を選ぶと media.sizes.large が存在しないことがあります。上の例のようにオプショナルチェーン(?.)で受けて、無ければ media.url にフォールバックしておくと安全です。
また、multiple を true にした場合、onSelect に渡ってくるのはオブジェクトではなく配列になります。単一選択のつもりで media.id を読むと undefined になるので、複数選択に切り替えるときは受け取り側も直す必要があります。
選択・変更・削除ができる edit.js を作る
ここまでの内容をまとめて、実際に使える形にします。画像が未選択なら選択ボタンだけを出し、選択済みならプレビュー画像と「画像を変更」「画像を削除」の2つのボタンを出す、という構成です。
import { useBlockProps, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
export default function Edit( { attributes, setAttributes } ) {
const { mediaId, mediaUrl, mediaAlt } = attributes;
const blockProps = useBlockProps();
// 画像が選ばれたとき
const onSelectImage = ( media ) => {
setAttributes( {
mediaId: media.id,
mediaUrl: media.url,
mediaAlt: media.alt ?? '',
} );
};
// 画像を外すとき(既定値に戻す)
const onRemoveImage = () => {
setAttributes( {
mediaId: 0,
mediaUrl: '',
mediaAlt: '',
} );
};
return (
<div { ...blockProps }>
{ mediaUrl && (
<img src={ mediaUrl } alt={ mediaAlt } style={ { maxWidth: '100%' } } />
) }
<MediaUploadCheck>
<MediaUpload
onSelect={ onSelectImage }
allowedTypes={ [ 'image' ] }
value={ mediaId }
render={ ( { open } ) => (
<Button
variant={ mediaUrl ? 'secondary' : 'primary' }
onClick={ open }
>
{ mediaUrl ? '画像を変更' : '画像を選択' }
</Button>
) }
/>
{ mediaUrl && (
<Button variant="link" isDestructive onClick={ onRemoveImage }>
画像を削除
</Button>
) }
</MediaUploadCheck>
</div>
);
}
削除ボタンでは属性を block.json の default と同じ値に戻しています。undefined を入れるのではなく明示的に初期値へ戻すことで、「画像なし」の状態が一意に決まり、条件分岐が単純になります。
なお、選択ボタンをブロック本体ではなくサイドバーに置きたい場合は、この MediaUploadCheck のかたまりを InspectorControls の中に移すだけで対応できます。MediaUpload はどこに置いても動作は変わりません。
save.js で img として書き出す
属性に入った値をフロント側の HTML にするのが save.js の役目です。画像が未選択のときは何も出力しないよう null を返しておきます。
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { mediaId, mediaUrl, mediaAlt } = attributes;
const blockProps = useBlockProps.save();
// 画像が選ばれていなければ何も出力しない
if ( ! mediaUrl ) {
return null;
}
return (
<figure { ...blockProps }>
<img
src={ mediaUrl }
alt={ mediaAlt }
// コアの画像ブロックと同じ wp-image-〇〇 クラスを付ける
className={ mediaId ? `wp-image-${ mediaId }` : undefined }
/>
</figure>
);
}
alt は必ず出力しておきましょう。装飾目的の画像で代替テキストが不要な場合でも、空文字の alt="" があるのとないのとでは、スクリーンリーダーの読み上げが変わります。
wp-image-〇〇 というクラスは、コアの画像ブロックが出力しているものと同じ形式です。テーマやプラグインがこのクラスを目印に処理していることがあるため、揃えておくと余計な相性問題を避けられます。
MediaPlaceholder ならもっと短く書ける
「画像が未選択のときに、ドラッグ&ドロップ領域つきのプレースホルダーを出す」という定番の見た目は、MediaPlaceholder を使えば自分で組み立てる必要がありません。コアの画像ブロックで画像を挿入する前に出てくる、あの灰色の枠と同じ UI が1つのコンポーネントで手に入ります。
import { useBlockProps, MediaPlaceholder } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const { mediaId, mediaUrl, mediaAlt } = attributes;
const blockProps = useBlockProps();
if ( ! mediaUrl ) {
return (
<div { ...blockProps }>
<MediaPlaceholder
icon="format-image"
labels={ {
title: '画像',
instructions: 'メディアライブラリから画像を選ぶか、アップロードしてください。',
} }
onSelect={ ( media ) =>
setAttributes( {
mediaId: media.id,
mediaUrl: media.url,
mediaAlt: media.alt ?? '',
} )
}
allowedTypes={ [ 'image' ] }
value={ { id: mediaId, src: mediaUrl } }
/>
</div>
);
}
return (
<figure { ...blockProps }>
<img src={ mediaUrl } alt={ mediaAlt } />
</figure>
);
}
MediaPlaceholder は内部で MediaUpload と MediaUploadCheck を使っているので、権限チェックも込みで面倒を見てくれます。ファイルをドラッグ&ドロップして直接アップロードする動線も最初から備わっています。
使い分けの目安としては、ブロックの主役が画像なら MediaPlaceholder、すでにある UI の一部としてボタンを1つ足したいだけなら MediaUpload と考えると分かりやすいでしょう。サイドバーのパネル内に「画像を選択」ボタンを置くようなケースでは、プレースホルダーの大きな枠は邪魔になるので MediaUpload が向いています。
ボタンを押してもモーダルが開かないとき
ボタンは表示されるのにクリックしても何も起きない、あるいはコンソールにエラーが出て編集画面が壊れる、という症状の原因はだいたい決まっています。
wp-block-editor の依存が指定されていない
@wordpress/scripts を使っている場合、import ... from '@wordpress/block-editor' はビルド時に wp.blockEditor への参照へ変換され、依存関係が index.asset.php に自動で書き出されます。block.json の editorScript でそのファイルを指定していれば、WordPress が依存を読み取って正しい順序でスクリプトを読み込んでくれます。
問題が起きやすいのは、ビルドを使わずに wp_register_script() で手書き登録している場合です。依存配列に wp-block-editor や wp-components を入れ忘れると、wp.blockEditor がまだ存在しない状態でコードが走り、MediaUpload が undefined になります。手動登録するなら次のように依存を明示します。
<?php
wp_register_script(
'my-blocks-figure',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components' ),
'1.0.0',
true
);
render の書き方が間違っている
render には「JSX を返す関数」を渡します。render={ <Button /> } のように要素そのものを渡すと open を受け取れず、当然モーダルも開きません。必ず render={ ( { open } ) => ( ... ) } の形にして、分割代入で open を受け取ってください。
また、onClick={ open() } と書いてしまうのもよくある間違いです。これは描画のたびに open を実行してしまう書き方なので、onClick={ open } と関数そのものを渡します。
権限のないユーザーで確認している
MediaUploadCheck で包んでいる以上、upload_files 権限のないユーザーではボタン自体が描画されません。「自分の管理者アカウントでは出るのに、テスト用アカウントでは出ない」という場合、それはバグではなく設計どおりの挙動です。表示されないユーザーの権限を確認してみてください。
選んだ画像がフロントに出ない・検証エラーになるとき
編集画面では画像が見えているのに公開ページには出てこない、あるいは記事を開き直すと「このブロックには、想定されていないエラーが含まれています」と表示される、という症状です。
属性に保存していない
onSelect の中で useState などのローカルな状態にだけ画像を入れていると、編集中は表示されますが投稿を保存した時点で消えます。フロントに出したい値は必ず setAttributes() でブロックの属性に入れてください。block.json の attributes にキーを定義し忘れている場合も、値は保存されずに捨てられます。
保存されているかどうかは、エディター右上のオプションメニューから「コードエディター」に切り替えると確認できます。ブロックコメントに {"mediaId":123,"mediaUrl":"..."} のような JSON が入っていれば保存できています。
save の出力を後から変更した
ブロック検証エラー(Block validation failed)は、すでに保存されている HTML と、いまの save.js が生成する HTML が食い違ったときに起こります。開発中に <img> を <figure> で包む形に変えた、クラス名を変えた、といった変更をすると、変更前に作った記事が軒並みエラーになります。
開発中でテスト記事しかないなら、そのブロックを削除して置き直すのが手っ取り早い解決です。すでに公開済みの記事で使われている場合は、古い save の実装を deprecated として登録しておくと、古い HTML も正しく読み込めるようになります。save の出力を変える予定があるなら、公開前に形を固めておくのが一番の予防策です。
画像の URL だけを保存して ID を捨てている
URL さえあれば表示はできますが、ID を保存していないと value による選択状態の復元ができず、あとから「この画像はどのメディアだったのか」を辿ることもできません。メディアライブラリ側で画像を差し替えたときの追従も効かなくなります。手間はほとんど変わらないので、id と url はセットで保存しておくのがおすすめです。
まとめ
カスタムブロックに画像選択の UI を付けるには、@wordpress/block-editor の MediaUpload を MediaUploadCheck で包んで使うのが基本です。render={ ( { open } ) => ... } でモーダルを開く関数を受け取り、onSelect で渡ってきたメディアオブジェクトの id と url を setAttributes() で属性に保存する。この流れさえ押さえれば、あとは save.js で <img> を書き出すだけです。allowedTypes で選べる種類を絞り、value に現在の ID を渡しておくと使い勝手が上がります。画像がブロックの主役になるようなケースでは、プレースホルダーの見た目と権限チェックをまとめて用意してくれる MediaPlaceholder を選ぶと、コードがぐっと短くなります。