WordPress Logo WordPress カスタムカードブロックの作り方

WordPress のブロックエディタ Gutenberg で MediaUpload コンポーネントを使って画像とタイトル及びテキストを表示するカスタムカードブロックを作成する方法の覚書です。

カスタムカードのタイトルはインスペクターでセレクトメニューを表示してタグを選択するようにすることもできます。

更新日:2020年10月20日

作成日:2020年10月08日

ブロックの基本的な作成方法や環境の構築方法については以下も御覧ください。

関連ページ

以下は Node.js がインストールされていて WordPress のローカル環境があることを前提にしています。

ファイルの作成

以下の例では JSX をコンパイルするための環境構築及び初期ファイル(雛形のファイル)の作成は create-block を使いプラグインとしてブロックを作成しています。

create-block を実行

ターミナルでプラグインディレクトリ wp-content/plugins に移動します。

以下は Mac で MAMP を使って WordPress のローカル環境を構築している場合の例です。パスは環境に合わせて適宜変更します。

$ cd /Applications/MAMP/htdocs/blocks/wp-content/plugins return

create-block を実行するコマンド npx @wordpress/create-block を実行します。環境の構築及びファイルの作成が完了するには数分かかります。

以下では対話モードで実行していますが、slug とオプションを指定して実行することもできます。

対話モードで設定できる主なオプション
オプション 説明 対話モードでの表示
slug 作成するプラグインのフォルダ(ファイルの出力先フォルダ)の名前に使用される文字列 ? The block slug used for identification (also the plugin and output folder name)
namespace 名前空間。ブロックをユニークに識別できる文字列 ? The internal namespace for the block name (something unique for your products)
title プラグインの名前(プラグインヘッダの Plugin Name)及びブロックの表示タイトル(registerBlockType の第2パラメータの title) ? The display title for your block
description ブロックの短い説明を指定。プラグインヘッダの Description 及び egisterBlockType の第2パラメータの description。 ? The short description for your block (optional)
dashicon ブロックのアイコン。registerBlockType の第2パラメータの icon。 ? The dashicon to make it easier to identify your block (optional)
category カテゴリー。registerBlockType の第2パラメータの category。 ? The category name to help users browse and discover your block

関連ページ:Gutenberg ブロック開発の環境構築

この例では slug に custom-card を指定して create-block を実行したので、custom-card というフォルダがプラグインディレクトリ(wp-content/plugins )に作成され、その中に以下のようなファイルが出力されます。

custom-card //作成されたプラグインのディレクトリ
├── block.json  
├── build //ビルドで出力されるファイル(本番環境で使用するファイル)のディレクトリ
│   ├── index.asset.php 
│   ├── index.css 
│   ├── index.js
│   └── style-index.css 
├── node_modules 
├── content-slider.php  //PHP 側でブロックを登録するプラグインファイル
├── package-lock.json
├── package.json 
├── readme.txt
└── src  //開発用ディレクトリ(この中のファイルを編集)
    ├── edit.js  //edit 関数(Edit コンポーネント)を記述するファイル
    ├── editor.scss  //エディター用スタイル
    ├── index.js  //ブロック用スクリプト(エントリーポイント)
    ├── save.js  //save 関数を記述するファイル
    └── style.scss  //フロントエンド及びエディターに適用するスタイル
開発モード

ターミナルで、作成されたプラグインのディレクトリに移動して npm start を実行して開発モードにして作業をしていきます。

開発モードの場合、ファイルの変更が自動的に検知され、ビルドが実行されるので、毎回ビルドコマンドを実行する必要がありません。何らかの理由でビルドが失敗するとターミナル及びコンソールにエラーが表示されます。

開発モードを終了するには control + c を押します。

$ cd custom-card  return  //作成されたプラグインのディレクトリに移動
  
$ npm start  return  //開発モード

開発が完了して procuction ビルドするには npm run build を実行します。

本番環境(サーバー)で必要になるのは build ディレクトリとその中の全てのファイルとメインのプラグインファイル content-slider.php になります。

ファイルの編集

以降の全ての編集作業はターミナルで npm start を実行して開発モードで行います。

この時点では、create-block を実行して自動的に生成された src フォルダにあるファイルを取り敢えず以下のように編集します。この例では翻訳関数は使用しないので翻訳関数及びその関連部分やコメントを削除しています。

src/index.js

エントリーポイントの src/index.js の先頭には必要なコンポーネントなどの読み込みが記述されています。

ブロックを登録・定義する registerBlockType の第1パラメータには create-block を実行する際に対話モードで指定した namespace と slug を使ったブロック名が設定されています。

第2パラメータのプロパティには対話モードで指定したオプションが設定されています。

edit 関数を指定する edit プロパティには先頭でインポートしている Edit コンポーネント(edit.js)が、save プロパティには save 関数が定義されている save.js が設定されています。

src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  save,  //save 関数は save.js を指定(save: save と指定したのと同じこと)
} );

src/edit.js

edit プロパティに指定する Edit コンポーネントにはエディター用ブロックのスタイルのインポート及びブロックのレンダリングを記述した Edit コンポーネントをデフォルトエクスポートしています。以下ではパラメータに props を受け取ってプロパティを分割代入で変数 className に設定しています。

src/edit.js
import './editor.scss';

export default function Edit( props ) {
  const { className } = props;
  return (
    <p className={ className }>Custom Card Block – エディター側</p>
  );
}

src/save.js

save プロパティに指定する save 関数が記述されています。

src/save.js
export default function save() {
  return (
    <p>Custom Card Block – フロントエンド側</p>
  );
}

プラグインページで作成したプラグインを有効化し、投稿に挿入すると以下のように表示されます。背景色や文字色は自動的に生成された editor.scss と style.scss に記述されているスタイルが適用されています。

以下はフロントエンド側の表示例です。

スタイルシート

自動的に生成された Sass ファイルの editor.scss と style.scss は以下のようになっています。

.wp-block-wdl-custom-card はブロックの外側の要素に自動的に指定されるクラス名です。

クラス名はブロック名(registerBlockType の第1パラメータ)の前に wp-block- を付け、名前空間セパレーターのスラッシュ( / )を - で置換して自動的に生成されます。

src/editor.scss
.wp-block-wdl-custom-card {
  border: 1px dotted #f00;
}
src/style.scss
.wp-block-wdl-custom-card {
  background-color: var(--wp-admin-theme-color);
  color: #fff;
  padding: 2px;
}
PHP でブロックを登録するファイル

以下は自動的に生成された PHP でブロックを登録するプラグインファイルです。

プラグインヘッダ部分には create-block を対話モードで指定したオプションが設定されています。

また、init アクションを使って、必要なファイルの登録(wp_register_script と wp_register_style)及び register_block_type() を使ったブロックの登録が記述されています。

$script_asset_path は create-block が自動的に生成した依存スクリプトの配列とバージョン(タイムスタンプ)が記述されたアセットファイルへのパスが格納されています。

ブロックのスクリプトの登録では、src/index.js、src/edit.js、src/save.js が webpack によってバンドル及びコンパイルされ build ディレクトリに出力された build/index.js を登録しています。

エディター用スタイル build/index.css は src/editor.scss がコンパイルされた CSS で、フロントエンド及びエディター用スタイル build/style-index.css は src/style.scss がコンパイルされた CSS です。

エントリーポイントの設定やビルド先などの設定は webpack.config.js という webpack の設定ファイルに記述されています。

PHP 側でブロックを登録する register_block_type() の第1パラメータには src/index.js に記述されている registerBlockType() の第1パラメータと同じブロック名が設定されています。

第2パラメータでは wp_register_script や wp_register_style で登録したスクリプトやスタイルをブロックに登録(関連付け)しています。

content-slider.php
<?php
/**
 * Plugin Name:     Custom Card Block
 * Description:     My First Custom Card Block Sample
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     custom-card
 *
 * @package         wdl
 */

// ブロックに必要なファイルの登録と register_block_type() を使ったブロックの登録
function wdl_custom_card_block_init() {
  $dir = dirname( __FILE__ );
  
  //アセットファイルのパス
  $script_asset_path = "$dir/build/index.asset.php";
  //アセットファイルのパスが存在しなければビルドが必要なことを伝えるエラーを投げる
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build`'
    );
  }
  
  //ブロックのスクリプトの登録
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-custom-card-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'], //アセットファイルに記述されている依存関係
    $script_asset['version'] //アセットファイルに記述されているバージョン
  );
  
  //この例では翻訳を使わないので以下は削除またはコメントアウト
  //wp_set_script_translations( 'wdl-custom-card-block-editor', 'custom-card' );

  //エディター用スタイルの登録
  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-custom-card-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  //フロントエンド及びエディター用スタイルの登録
  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-custom-card-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  //ブロックを登録する関数(上記で登録したスクリプトやスタイルをブロックに関連付け)
  register_block_type( 'wdl/custom-card', array(
    'editor_script' => 'wdl-custom-card-block-editor',
    'editor_style'  => 'wdl-custom-card-block-editor',
    'style'         => 'wdl-custom-card-block',
  ) );
}
add_action( 'init', 'wdl_custom_card_block_init' );

attributes を設定

このブロックでは MediaUpload コンポーネントを使って画像のアップロードボタン及び画像がアップロードされたら画像を表示し、RichText コンポーネントを使ってタイトルとテキストを表示します。

以下の属性を src/index.js の registerBlockType() の attributes プロパティに設定します。

  • title:RichText のタイトルを保持する属性。
  • body:RichText のテキストを保持する属性。
  • mediaID:MediaUpload の value の値を保持する属性。画像を削除する際に使用。
  • imageUrl:MediaUpload の画像の src 属性の値を保持する属性。
  • imageAlt:MediaUpload の画像の alt 属性の値を保持する属性。

title と body では source に children(子要素)を指定し、selector に save 関数で記述する要素のクラス .card_title や .card_body を指定しています。

imageUrl と imageAlt では source に attribute(要素の属性)を指定し、selector に save 関数で記述する img 要素のクラス .card_image を指定しています。

src/index.js
//属性を設定
attributes: {
  //RichText のタイトル
  title: {
    type: 'array',
    source: 'children',
    selector: '.card_title',
  },
  //RichText のテキスト(文章)
  body: {
    type: 'array',
    source: 'children',
    selector: '.card_body'
  },
  //MediaUpload の value の値
  mediaID: {
    type: 'number',
    default: 0
  },
  //img の src に指定する URL
  imageUrl: {
    type: 'string',
    source: 'attribute',
    attribute: 'src',
    selector: '.card_image'
  },
  //img の alt 属性の値 
  imageAlt: {
    type: 'string',
    source: 'attribute',
    attribute: 'alt',
    selector: '.card_image'
  },
},
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  //属性を設定
  attributes: {
    //RichText のタイトル
    title: {
      type: 'array',
      source: 'children',
      selector: '.card_title',
    },
    //RichText のテキスト(文章)
    body: {
      type: 'array',
      source: 'children',
      selector: '.card_body'
    },
    //MediaUpload の value の値
    mediaID: {
      type: 'number',
      default: 0
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'string',
      source: 'attribute',
      attribute: 'src',
      selector: '.card_image'
    },
    //img の alt 属性の値 
    imageAlt: {
      type: 'string',
      source: 'attribute',
      attribute: 'alt',
      selector: '.card_image'
    },
  },
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  save,  //save 関数は save.js を指定(save: save と指定したのと同じこと)
} );

Edit コンポーネント

エディター画面でのブロックのレンダリングを定義する edit 関数を Edit コンポーネント(src/edit.js)に記述します。

src/edit.js
import { RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
import './editor.scss';

export default function Edit( props ) {
  //分割代入を使って props 経由でプロパティを変数に代入
  const { className, attributes, setAttributes} = props;
  
  //選択された画像の情報(alt 属性、URL、ID)を更新する関数
  const onSelectImage = ( media ) => {
    setAttributes( {
      imageAlt: media.alt, 
      imageUrl: media.url, 
      mediaID: media.id 
    } );
  };
  
  //メディアライブラリを開くボタンをレンダリングする関数
  const getImageButton = (open) => {
    if(attributes.imageUrl) {
      return (
        <img 
          src={ attributes.imageUrl }
          onClick={ open }
          className="image"
          alt="アップロード画像"
        />
      );
    }
    else {
      return (
        <div className="button-container">
          <Button 
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };
  
  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: 0,
      imageUrl: '',
      imageAlt: ''
    });
  }

  return (
    <div className={"container " + className}>
      <MediaUploadCheck>
        <MediaUpload
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton(open) }
        />
      </MediaUploadCheck>
      { attributes.mediaID != 0  && 
        <MediaUploadCheck>
          <Button 
            onClick={removeMedia} 
            isLink 
            isDestructive 
            className="removeImage">画像を削除
          </Button>
        </MediaUploadCheck>
      }
      <RichText
        onChange={ value => setAttributes({ title: value }) }
        value={ attributes.title }
        tagName='h3'
        placeholder="カードのタイトル"
        keepPlaceholderOnFocus={true}
        className="heading"
      />
      <RichText
        onChange={ value => setAttributes({ body: value }) }
        value={ attributes.body }
        multiline="p"
        placeholder="カードのテキスト"
      />
    </div>
  );
}

まず、使用する WordPress のコンポーネントをコンポーネントが含まれるパッケージから import を使ってインポートします。

RichText、 MediaUpload、 MediaUploadCheck コンポーネントは block-editor パッケージからインポートし、Button コンポーネントは components パッケージからインポートします。

import { RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';

続いて分割代入を使って props から選択的にプロパティを変数に代入しています。

const { className, attributes, setAttributes} = props;

onSelectImage は MediaUpload コンポーネントの onSelect プロパティに指定するコールバック関数で、選択されたメディアのオブジェクト(media)が引数として渡されます。

メディア(この場合は画像)が選択されたら、引数に渡された画像オブジェクトの alt、url、id プロパティを setAttributes を使ってそれぞれの属性に設定しています。

const onSelectImage = ( media ) => {
  setAttributes( {
    imageAlt: media.alt, 
    imageUrl: media.url, 
    mediaID: media.id 
  } );
};

getImageButton は MediaUpload コンポーネントの render プロパティに指定する「メディアライブラリを開くボタンをレンダリングするために呼び出される」コールバック関数です。

このコールバック関数は引数にコンポーネント側で定義されているモーダルウィンドウを開く関数 open を受け取ります。以下では、属性 attributes.imageUrl が取得されていれば img 要素をレンダリングし、取得されていなければ Button コンポーネントで画像をアップロードするためのボタンをレンダリングします。

onClick プロパティには画像を選択するモーダルウィンドウを開く関数 open を指定し、画像やボタンをクリックすると画像を選択するモーダルウィンドウを開くようにしています。

const getImageButton = (open) => {
  if(attributes.imageUrl) { //属性 imageUrl が取得されていれば img 要素をレンダリング
    return (
      <img 
        src={ attributes.imageUrl }
        onClick={ open }
        className="image"
        alt="アップロード画像"
      />
    );
  }
  else { //属性 imageUrl が取得されていなければ、Button コンポーネントでボタンをレンダリング
    return (
      <div className="button-container">
        <Button 
          onClick={ open }
          className="button button-large"
        >
          画像をアップロード
        </Button>
      </div>
    );
  }
};

removeMedia は画像を削除するためのボタンの onClick プロパティに指定するメディアをリセット(画像を削除)するコールバック関数です。

この関数が呼び出されると setAttributes を使って属性をリセットします。imageUrl を ''(空)にすると img 要素の src 属性が空になるので画像は削除されます。

const removeMedia = () => {
  setAttributes({
    mediaID: 0,
    imageUrl: '',
    imageAlt: ''
  });
}

return ステートメントでは、エディター画面のブロックのレンダリングを JSX を使って記述しています。HTML のように見えますが、実際は JSX で記述された React 要素で、JavaScript の式です。

MediaUpload コンポーネントを使う場合は、ユーザがメディアライブラリを使う権限があることをチェックする MediaUploadCheck コンポーネントでラップします。

カードのタイトルをレンダリングする RichText コンポーネントでは tagName プロパティを指定して h3 要素で表示するようにしています。

カードのテキストをレンダリングする RichText コンポーネントでは multiline プロパティを指定して return キーを押すと改行ではなく、段落を生成するようにしています。

onChange プロパティでは入力された値が変更されると setAttributes を使って属性の値を更新し、それにより value プロパティの値が更新されるようになっています。

関連項目:MediaUpload と Button コンポーネントのプロパティ

return (
  <div className={"container " + className}>
    <MediaUploadCheck>
      <MediaUpload
        onSelect={ onSelectImage }
        allowedTypes={ ['image'] } //選択できるメディアのタイプを配列で指定
        value={ attributes.mediaID } //選択したメディアの Media ID 
        render={ ({ open }) => getImageButton(open) }
      />
    </MediaUploadCheck>
    { attributes.mediaID != 0  && 
      <MediaUploadCheck>
        <Button 
          onClick={removeMedia} 
          isLink  //アンカースタイルのボタン
          isDestructive //赤いテキストベースのスタイル
          className="removeImage">画像を削除
        </Button>
      </MediaUploadCheck>
    }
    <RichText
      onChange={ value => setAttributes({ title: value }) }
      value={ attributes.title }
      tagName='h3'
      placeholder="カードのタイトル"
      keepPlaceholderOnFocus={true}
      className="heading"
    />
    <RichText
      onChange={ value => setAttributes({ body: value }) }
      value={ attributes.body }
      multiline="p"
      placeholder="カードのテキスト"
    />
  </div>
);

この時点では、エディター側は以下のように表示されます。

「画像をアップロード」というボタンをクリックして画像を挿入することができ、タイトルやテキスト部分に文字を入力することができますが、更新ボタンをクリックしても save 関数を定義していないので保存はされません。

save 関数

save プロパティに指定する save 関数では、フロントエンド側でブロックがどのようにレンダリングされるかを定義して返します(return します)。

src/save.js
export default function save( { attributes } ) {

  //画像をレンダリングする関数
  const cardImage = (src, alt) => {
    if(!src) return null;

    if(alt) {
      return (
        <img 
          className="card_image" 
          src={ src }
          alt={ alt }
        /> 
      );
    }
    
    //alt 属性が設定されていないので、スクリーンリーダーでは aria-hidden で隠す
    return (
      <img 
        className="card_image" 
        src={ src }
        alt=""
        aria-hidden="true"
      /> 
    );
  };
  
  return (
    <div className="card">
      { cardImage(attributes.imageUrl, attributes.imageAlt) }
      <div className="card_content">
        <h3 className="card_title">{ attributes.title }</h3>
        <div className="card_body">
          { attributes.body }
        </div>
      </div>
    </div>
  );
}

画像をレンダリングする関数 cardImage を定義しています。この関数は引数に attributes に保持されている画像の src と alt 属性の値を取り、それらの値により出力を切り替えます。

src(attributes.imageUrl)が設定されていなければ、画像はまだ選択されていないので、null を返します。何もレンダリングしない場合、JSX では null を返します。

alt(attributes.imageAlt)が設定されていれば、挿入された画像には alt 属性が設定されているので、その値をレンダリングする画像の alt 属性に設定した img 要素を返します。設定されていない場合は、alt 属性を空で設定し、aria-hidden="true" を設定し、スクリーンリーダーではこの要素が非表示であることをブラウザに伝えます。

また、属性 imageUrl と imageAlt の selector プロパティに指定したクラス(card_image)を設定しています。

//画像をレンダリングする関数
const cardImage = (src, alt) => {
  if(!src) return null;

  if(alt) {
    return (
      <img 
        className="card_image" 
        src={ src }
        alt={ alt }
      /> 
    );
  }

  //alt 属性が設定されていないので、スクリーンリーダーでは aria-hidden で隠す
  return (
    <img 
      className="card_image" 
      src={ src }
      alt=""
      aria-hidden="true"
    /> 
  );
};

return ステートメントでは、関数 cardImage で画像をレンダリングし、RichText コンポーネントに入力されたタイトルを h3 要素で 、テキストを div 要素でレンダリングしています。それぞれには属性(attributes)の selector で指定したクラスを設定しています。

return (
  <div className="card">
    { cardImage(attributes.imageUrl, attributes.imageAlt) }
    <div className="card_content">
      <h3 className="card_title">{ attributes.title }</h3>
      <div className="card_body">
        { attributes.body }
      </div>
    </div>
  </div>
);

妥当性検証プロセス

変更後、投稿のページを再読み込みすると以下のように「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示され、コンソールにはエラーが表示されます。

これは、エディターが現在定義しているものとは異なる save 関数の出力を検出するために発生します。

※ 静的なブロックでは save 関数の return ステートメント内に変更が発生するとブロックの検証(妥当性検証プロセス)により、ブロックは無効(invalid)としてマークされ上記のように表示されます。

この場合、表示されるボタンをクリックして「ブロックを削除」を選択し、現在のブロックを一度削除してから再度ブロックを挿入します。

再度ブロックを挿入すると以下のように表示されます。

画像を挿入してタイトルやテキストを入力し、「更新」ボタンをクリックして保存すると save 関数を設定しているのでフロントエンド側にも変更が反映されます。

スタイルの設定

create-block を実行した場合、エディター用のスタイルは src ディレクトリの editor.scss を、フロントエンド及びエディターに適用されるスタイルは style.scss に記述します。

この時点ではそれぞれのスタイルシートには create-block で生成された際のスタイルが設定されているので、レンダリングのマークアップで指定したクラス名などを使って好みのスタイルに変更します。

以下はエディター用のスタイル src/editor.scss の例です。

src/editor.scss
$gray: #cccccc;
$off-white: #f1f1f1;

.container .image {
  cursor: pointer;
}

.container {
  border: 1px solid $gray;
  padding: 1rem;
}

.button-container {
  text-align: center;
  padding: 22% 0;
  background: $off-white;
  border: 1px solid $gray;
  border-radius: 2px;
  margin: 0 0 1.2rem 0;
}

.heading {
  font-size: 1.5rem;
  font-weight: 600;
}

.image {
  height: 10rem;
  width: 100%;
  object-fit: cover;
}

エディター画面で挿入された画像は以下のように、onClick プロパティを指定してクリックするとモーダルウィンドウを開くようになっているので、.container .image に cursor: pointer; を設定して画像がクリックできることをわかるようにしています。

<img 
  src={ attributes.imageUrl }
  onClick={ open }
  className="image"
  alt="アップロード画像"
/>

以下はフロントエンド及びエディター用のスタイル src/style.scss の例です。

src/style.scss
.wp-block-wdl-custom-card {
  background-color: #fefefe;
  color: #999;
  padding: 10px;
}

上記スタイル設定後のエディター画面でのブロックは以下のような表示になります。

本番用ビルドの実行

問題なくブロックが作成できたら、開発モードを終了し、以下のビルドコマンドを実行します。開発モードを終了するには control + c を押します。

$ npm run build  return  //ビルドを実行

上記ビルドコマンドを実行すると、build ディレクトリに出力されるファイルは最適化及び圧縮されます。

本番環境(サーバー)で必要になるのは build ディレクトリとその中の全てのファイルとメインのプラグインファイル content-slider.php になります。

この例の場合、build ディレクトリには、index.asset.php、index.css、index.js、style-index.css の4つのファイルが出力されます。

$ npm run build

> custom-card@0.1.0 build /Applications/MAMP/htdocs/blocks/wp-content/plugins/custom-card
> wp-scripts build

Hash: c5d89b50a9d2bca2d1fc
Version: webpack 4.44.2
Time: 1963ms
Built at: 2020/10/08 10:19:10
          Asset       Size  Chunks             Chunk Names
index.asset.php  171 bytes       0  [emitted]  index
      index.css  320 bytes       0  [emitted]  index
       index.js   4.02 KiB       0  [emitted]  index
style-index.css   77 bytes       1  [emitted]  style-index
Entrypoint index = style-index.css style-index.js index.css index.js index.asset.php
[0] external {"this":["wp","element"]} 42 bytes {0} [built]
[1] external {"this":["wp","blockEditor"]} 42 bytes {0} [built]
[2] external {"this":["wp","components"]} 42 bytes {0} [built]
[3] external {"this":["wp","blocks"]} 42 bytes {0} [built]
[4] ./src/style.scss 39 bytes {1} [built]
[5] ./src/editor.scss 39 bytes {0} [built]
[6] ./src/index.js + 2 modules 4.7 KiB {0} [built]
    | ./src/index.js 1.25 KiB [built]
    | ./src/edit.js 2.54 KiB [built]
    | ./src/save.js 902 bytes [built]
    + 2 hidden modules

タイトルタグのオプションを追加

この例ではカードに表示するタイトルは h3 要素でレンダリングしていますが、インスペクターにセレクトメニューを表示してタイトルに使用する要素を選択できるようにする例です。

src/index.js で registerBlockType() の attributes プロパティにタイトルに使用する要素(h2〜h5)を保持する属性 headingElem を追加します。

attributes: {
  ・・・中略・・・
  //タイトルの要素(h2〜h5) 
  headingElem: {
    type: 'string',
    default: 'h3'
  },
},
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  //属性を設定
  attributes: {
    //RichText のタイトル
    title: {
      type: 'array',
      source: 'children',
      selector: '.card_title',
    },
    //RichText のテキスト(文章)
    body: {
      type: 'array',
      source: 'children',
      selector: '.card_body'
    },
    //MediaUpload の value の値
    mediaID: {
      type: 'number',
      default: 0
    },
    //img の src に指定する URL
    imageUrl: {
      type: 'string',
      source: 'attribute',
      attribute: 'src',
      selector: '.card_image'
    },
    //img の alt 属性の値 
    imageAlt: {
      type: 'string',
      source: 'attribute',
      attribute: 'alt',
      selector: '.card_image'
    },
    //タイトルの要素(h2〜h5) 
    headingElem: {
      type: 'string',
      default: 'h3'
    },
  },
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  save,  //save 関数は save.js を指定(save: save と指定したのと同じこと)
} );

インスペクターの追加

エディター画面にインスペクターを追加し、その中にセレクトメニューを配置します。

インスペクターとセレクトメニューを追加するには Editor コンポーネント(edit.js)で、InspectorControls コンポーネントを block-editor パッケージから、PanelBody、PanelRow、そしてセレクトメニューの SelectControl コンポーネントを components パッケージからインポートします。

// InspectorControls を block-editor パッケージから追加でインポート
import { InspectorControls, RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
// SelectControl, PanelBody, PanelRow を components パッケージから追加でインポート
import { SelectControl, PanelBody, PanelRow , Button } from '@wordpress/components';

この例では以下のようなインスペクターを追加する関数を作成してレンダリングします。

SelectControl の options プロパティにはセレクトメニューで表示するオプションのラベルと値のオブジェクトを指定し、onChange プロパティで選択された値を属性 headingElem に設定します。

const getInspectorControls = () => {
  return (
    <InspectorControls>
      <PanelBody
        title="カードオプション" //パネルのタイトル
        initialOpen={true} //初期状態でパネルを開いて表示
      >
        <PanelRow>
          <SelectControl
            label="タイトルタグ"
            help="タイトルに使用する要素(h2〜h5)"
            value={attributes.headingElem}
            options={[
              {label: "h2", value: 'h2'},
              {label: "h3", value: 'h3'},
              {label: "h4", value: 'h4'},
              {label: "h5", value: 'h5'},
            ]}
            onChange={(val) => setAttributes({ headingElem: val })}
          />
        </PanelRow>
      </PanelBody>
    </InspectorControls>
  );
}

関連項目

return ステートメントでは、インスペクターを関数を使ってレンダリングするので、配列として記述します。

タイトル要素をレンダリングする RichText の tagName プロパティにセレクトメニューで選択されたタグ(attributes.headingElem)を指定します。

return (
  [
    getInspectorControls(), //インスペクター
    <div className={"container " + className}>
      <MediaUploadCheck>
        <MediaUpload
          onSelect={ onSelectImage }
          allowedTypes={ ['image'] }
          value={ attributes.mediaID }
          render={ ({ open }) => getImageButton(open) }
        />
      </MediaUploadCheck>
      { attributes.mediaID != 0  && 
        <MediaUploadCheck>
          <Button 
            onClick={removeMedia} 
            isLink 
            isDestructive 
            className="removeImage">画像を削除
          </Button>
        </MediaUploadCheck>
      }
      <RichText
        onChange={ value => setAttributes({ title: value }) }
        value={ attributes.title }
        tagName={ attributes.headingElem } //タグを属性から取得
        placeholder="カードのタイトル"
        keepPlaceholderOnFocus={true}
        className="heading"
      />
      <RichText
        onChange={ value => setAttributes({ body: value }) }
        value={ attributes.body }
        multiline="p"
        placeholder="カードのテキスト"
      />
    </div>
  ]
);
src/edit.js
import { InspectorControls, RichText, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { SelectControl, PanelBody, PanelRow , Button } from '@wordpress/components';
import './editor.scss';

export default function Edit( props ) {
  const { className, attributes, setAttributes} = props;
  
  //選択された画像の情報(alt 属性、URL、ID)を更新する関数
  const onSelectImage = ( media ) => {
    setAttributes( {
      imageAlt: media.alt, 
      imageUrl: media.url, 
      mediaID: media.id 
    } );
  };
  
  //メディアライブラリを開くボタンをレンダリングする関数
  const getImageButton = (open) => {
    if(attributes.imageUrl) {
      return (
        <img 
          src={ attributes.imageUrl }
          onClick={ open }
          className="image"
          alt="アップロード画像"
        />
      );
    }
    else {
      return (
        <div className="button-container">
          <Button 
            onClick={ open }
            className="button button-large"
          >
            画像をアップロード
          </Button>
        </div>
      );
    }
  };
  
  //画像を削除する(メディアをリセットする)関数
  const removeMedia = () => {
    setAttributes({
      mediaID: 0,
      imageUrl: '',
      imageAlt: ''
    });
  }
  
  //インスペクターを追加する関数
  const getInspectorControls = () => {
    return (
      <InspectorControls>
        <PanelBody
          title="カードオプション" //パネルのタイトル
          initialOpen={true} //初期状態でパネルを開いて表示
        >
        <PanelRow>
          <SelectControl
            label="タイトルタグ"
            help="タイトルに使用する要素(h2〜h5)"
            value={attributes.headingElem}
            options={[
              {label: "h2", value: 'h2'},
              {label: "h3", value: 'h3'},
              {label: "h4", value: 'h4'},
              {label: "h5", value: 'h5'},
            ]}
            onChange={(val) => setAttributes({ headingElem: val })}
          />
        </PanelRow>
        </PanelBody>
      </InspectorControls>
    );
  }
  
  return (
    [
      getInspectorControls(), //インスペクター
      <div className={"container " + className}>
        <MediaUploadCheck>
          <MediaUpload
            onSelect={ onSelectImage }
            allowedTypes={ ['image'] }
            value={ attributes.mediaID }
            render={ ({ open }) => getImageButton(open) }
          />
        </MediaUploadCheck>
        { attributes.mediaID != 0  && 
          <MediaUploadCheck>
            <Button 
              onClick={removeMedia} 
              isLink 
              isDestructive 
              className="removeImage">画像を削除
            </Button>
          </MediaUploadCheck>
        }
        <RichText
          onChange={ value => setAttributes({ title: value }) }
          value={ attributes.title }
          tagName={ attributes.headingElem } //タグを属性から取得
          placeholder="カードのタイトル"
          keepPlaceholderOnFocus={true}
          className="heading"
        />
        <RichText
          onChange={ value => setAttributes({ body: value }) }
          value={ attributes.body }
          multiline="p"
          placeholder="カードのテキスト"
        />
      </div>
    ]
  );
}

save 関数

save 関数では、属性 headingElem から選択されたタイトルのタグを取得して createElement を使ってタイトルの React 要素を生成してレンダリングします。

createElement を使用するには element パッケージから createElement をインポートします。

//createElement を element パッケージからインポート
import { createElement } from '@wordpress/element';

選択されたタイトルのタグとタイトルに入力された文字を属性から取得して変数に代入します。

createElement の第1パラメータ(type)に選択されたタイトルのタグの文字列を指定します。

第2パラメータにはプロパティを、第3プロパティには子要素である入力された文字列を指定して、選択されたタグ(例えば h4)の React 要素を生成します。

return ステートメントでは生成されたタイトルの React 要素(title_element)を使ってレンダリングします。

//選択されたタイトルのタグ(h2〜h5)
let heading_element = attributes.headingElem;
//タイトルに入力された文字
let title_content = attributes.title;
//タイトルをレンダリングする React 要素
const title_element = createElement(
  heading_element,  
  { className: "card_title" },  
  title_content
)

return (
  <div className="card">
    { cardImage(attributes.imageUrl, attributes.imageAlt) }
    <div className="card_content">
      { title_element }
      <div className="card_body">
        { attributes.body }
      </div>
    </div>
  </div>
);
src/save.js
import { createElement } from '@wordpress/element';

export default function save( { attributes } ) {

  const cardImage = (src, alt) => {
    if(!src) return null;

    if(alt) {
      return (
        <img 
          className="card_image" 
          src={ src }
          alt={ alt }
        /> 
      );
    }
    
    //alt 属性が設定されていないので、スクリーンリーダーでは aria-hidden で隠す
    return (
      <img 
        className="card_image" 
        src={ src }
        alt=""
        aria-hidden="true"
      /> 
    );
  };
  
  //選択されたタイトルのタグ(h2〜h5)
  let heading_element = attributes.headingElem;
  //タイトルに入力された文字
  let title_content = attributes.title;
  //タイトルをレンダリングする React 要素
  const title_element = createElement(
    heading_element,  
    { className: "card_title" },  
    title_content
  )
  
  return (
    <div className="card">
      { cardImage(attributes.imageUrl, attributes.imageAlt) }
      <div className="card_content">
        { title_element }
        <div className="card_body">
          { attributes.body }
        </div>
      </div>
    </div>
  );
}

PHP でレンダリング

以下はブロックをダイナミックブロックに変更して PHP でレンダリングする例です。

PHP でレンダリングする場合、PHP 側の出力(マークアップ)を変更しても妥当性検証プロセスは行われないので、マークアップを変更する際に発生するブロックを一度削除してから再度ブロックを挿入しなければならない問題を回避できます。

例えばブロックにタイトルを追加すれば既存のブロック全てにタイトルが追加され、その際には JavaScript 側の再コンパイルは不要です。

関連ページ:ダイナミックブロックの作り方

save 関数で null を返す

ダイナミックブロックでは save 関数が null を返すようにします(save.js は不要になります)。

src/index.js 抜粋
registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  attributes: {
    ・・・中略・・・
  },
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  //save 関数で null を返す
  save: () => { return null }
} );

attributes / source プロパティの削除

attributes に設定した属性で source プロパティを設定していると、ブロックのデータを save 関数の出力の属性から取得するめ、PHP でレンダリングされるブロックではサポートされていません(save 関数で null を返しているため)。

このため属性の source プロパティを削除し、selector プロパティも不要になるので削除します。

また、属性の type が array のものは、source プロパティを削除すると機能しないため、type を string に変更します。

または、この例の場合、属性を PHP 側でも設定するため、JavaScript 側の attributes を全て削除しても同じです(edit 関数では PHP 側で設定した attributes に props 経由でアクセスできます)。そのため、この例では JavaScript 側の attributes を全て削除します(残しておいても問題ありません。また、インスペクターの初期値などは JavaScript 側での設定が反映される場合があるようなので、そのような場合は残します)。

src/index.js
registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  //属性を削除 または各属性の source プロパティを削除し type を適宜変更
  /*attributes: {
    //RichText のタイトル
    title: {
      type: 'string', //array から変更
    },
    //RichText のテキスト(文章)
    body: {
      type: 'string', //array から変更
    },
    //MediaUpload の value の値(選択された画像から取得)
    mediaID: {
      type: 'number',
      default: 0
    },
    //MediaUpload の画像の URL(選択された画像から取得)
    imageUrl: {
      type: 'string',
    },
    //img の alt 属性の値 
    imageAlt: {
      type: 'string',
    },
    //タイトルの要素(h2〜h5) 
    headingElem: {
      type: 'string',
      default: 'h3'
    },
  },*/
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  //save 関数で null を返す
  save: () => { return null }
} );
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';

registerBlockType( 'wdl/custom-card', {
  title: 'Custom Card Block',
  description: 'My First Custom Card Block Sample',
  category: 'common',
  icon: 'smiley',
  edit: Edit,  //edit 関数は Edit コンポーネント(edit.js)を指定
  //save 関数で null を返す
  save: () => { return null }
} );

register_block_type に attributes を追加

register_block_type 関数の第2パラメータの配列に attributes キーを追加して、ブロックから抽出したいすべての属性を PHP 側で設定することができます。

register_block_type に attributes を設定する場合は、オブジェクトではなく連想配列で設定する必要があります。また、default を設定して render_callback の関数で属性を使用する際に値が未定義にならないようにします。

custom-card.php 抜粋
register_block_type( 'wdl/custom-card', array(
  'editor_script' => 'wdl-custom-card-block-editor',
  'editor_style'  => 'wdl-custom-card-block-editor',
  'style'         => 'wdl-custom-card-block',
  //属性を追加
  'attributes' => [
    //RichText のタイトル
    'title' => [
      'type' => 'string', 
      'default' =>''
    ],
    //RichText のテキスト(文章)
    'body' => [
      'type' => 'string', 
      'default' =>''
    ],
    //MediaUpload の value の値
    'mediaID' => [
      'type' => 'number', 
      'default' => 0 
    ],
    //MediaUpload の画像の URL
    'imageUrl' => [
      'type' => 'string', 
      'default' =>''
    ],
    //img の alt 属性の値 
    'imageAlt' => [
      'type' => 'string', 
      'default' =>''
    ],
    //タイトルの要素(h2〜h5) 
    'headingElem' => [
      'type' => 'string', 
      'default' =>'h3'
    ],
  ], 
) );

render_callback を設定

ブロックの出力を PHP でレンダリングするには、register_block_type 関数の第2パラメータの配列に render_callback キーを追加し、値にコールバック関数(PHP でレンダリングする関数)を指定します。

custom-card.php 抜粋
register_block_type( 'wdl/custom-card', array(
  'editor_script' => 'wdl-custom-card-block-editor',
  'editor_style'  => 'wdl-custom-card-block-editor',
  'style'         => 'wdl-custom-card-block',
  //render_callback を追加
  'render_callback' => 'custom_card_render',
  'attributes' => [
    'title' => [
      'type' => 'string', 
    ],
    ・・・省略・・・
) );

render_callback に指定した PHP でレンダリングする関数 custom_card_render を定義します。

PHP 側では属性は連想配列で設定しているので、例えば、画像の URL が保持されている属性 imageUrl には $attributes["imageUrl"] でアクセスできます。

但し、attributes で初期値(default)が設定されていないと $attributes["xxxx"] は値が設定されていない場合に未定義になるので、isset($attr['xxxx']) でのチェックが必要になります。

custom-card.php 抜粋
function custom_card_render($attributes, $content) {
  $output = '<div class="card wp-block-wdl-custom-card">';
  $card_image = '';
  $imageUrl = esc_url($attributes["imageUrl"]);
  $imageAlt = esc_attr($attributes["imageAlt"]);
  $headingElem = esc_html($attributes["headingElem"]);
  $title = esc_html($attributes["title"]);
  $body = $attributes["body"];
  
  if($imageUrl) {
    if($imageAlt) {
      // alt 属性が画像に設定されている場合の img のマークアップ
      $card_image = '<img class="card_image" src="' .$imageUrl. '" alt="' .$imageAlt. '"/>';
    }else {
      // alt 属性が画像に設定されていない場合の img のマークアップ
      $card_image = '<img class="card_image" src="' .$imageUrl. '" alt="" aria-hidden="true"/>';
    }
  }
  
  $output .= $card_image;
  $output .= '<div class="card_content">';
  //タイトルの要素(h2〜h5) 
  $output .= '<' . $headingElem . ' class: "card_title">' . $title . '</' . $headingElem . '>';
  $output .= '<div class="card_body">'. $body .'</div></div></div>';
  return $output;
}

投稿のページを再読込すると妥当性検証プロセスにより「このブロックには、想定されていないか無効なコンテンツが含まれています」と表示されるので、現在のブロックを一度削除してから再度ブロックを挿入します。以降の PHP 側でのレンダリング(マークアップ)の変更では妥当性検証プロセスは行われなくなります。

custom-card.php
<?php
/**
 * Plugin Name:     Custom Card Block
 * Description:     My First Custom Card Block Sample
 * Version:         0.1.0
 * Author:          The WordPress Contributors
 * License:         GPL-2.0-or-later
 * License URI:     https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:     custom-card
 *
 * @package         wdl
 */


function wdl_custom_card_block_init() {
  $dir = dirname( __FILE__ );

  $script_asset_path = "$dir/build/index.asset.php";
  if ( ! file_exists( $script_asset_path ) ) {
    throw new Error(
      'You need to run `npm start` or `npm run build`'
    );
  }
  $index_js     = 'build/index.js';
  $script_asset = require( $script_asset_path );
  wp_register_script(
    'wdl-custom-card-block-editor',
    plugins_url( $index_js, __FILE__ ),
    $script_asset['dependencies'],
    $script_asset['version']
  );

  $editor_css = 'build/index.css';
  wp_register_style(
    'wdl-custom-card-block-editor',
    plugins_url( $editor_css, __FILE__ ),
    array(),
    filemtime( "$dir/$editor_css" )
  );

  $style_css = 'build/style-index.css';
  wp_register_style(
    'wdl-custom-card-block',
    plugins_url( $style_css, __FILE__ ),
    array(),
    filemtime( "$dir/$style_css" )
  );

  register_block_type( 'wdl/custom-card', array(
    'editor_script' => 'wdl-custom-card-block-editor',
    'editor_style'  => 'wdl-custom-card-block-editor',
    'style'         => 'wdl-custom-card-block',
    //render_callback を追加
    'render_callback' => 'custom_card_render',
    //属性を追加
    'attributes' => [
      //RichText のタイトル
      'title' => [
        'type' => 'string', 
        'default' =>''
      ],
      //RichText のテキスト(文章)
      'body' => [
        'type' => 'string', 
        'default' =>''
      ],
      //MediaUpload の value の値
      'mediaID' => [
        'type' => 'number', 
        'default' => 0 
      ],
      //MediaUpload の画像の URL
      'imageUrl' => [
        'type' => 'string', 
        'default' =>''
      ],
      //img の alt 属性の値 
      'imageAlt' => [
        'type' => 'string', 
        'default' =>''
      ],
      //タイトルの要素(h2〜h5) 
      'headingElem' => [
        'type' => 'string', 
        'default' =>'h3'
      ],
    ],  
  ) );
}
add_action( 'init', 'wdl_custom_card_block_init' );

function custom_card_render($attributes, $content) {
  $output = '<div class="card wp-block-wdl-custom-card">';
  $card_image = '';
  $imageUrl = esc_url($attributes["imageUrl"]);
  $imageAlt = esc_attr($attributes["imageAlt"]);
  $headingElem = esc_html($attributes["headingElem"]);
  $title = esc_html($attributes["title"]);
  $body = $attributes["body"];
  
  if($imageUrl) {
    if($imageAlt) {
      // alt 属性が画像に設定されている場合の img のマークアップ
      $card_image = '<img class="card_image" src="' .$imageUrl. '" alt="' .$imageAlt. '"/>';
    }else {
      // alt 属性が画像に設定されていない場合の img のマークアップ
      $card_image = '<img class="card_image" src="' .$imageUrl. '" alt="" aria-hidden="true"  />';
    }
  }
  
  $output .= $card_image;
  $output .= '<div class="card_content">';
  //タイトルの要素(h2〜h5) 
  $output .= '<' . $headingElem . ' class: "card_title">' . $title . '</' . $headingElem . '>';
  $output .= '<div class="card_body">'. $body .'</div></div></div>';
  return $output;
}

参考にさせていただいたサイト:Learning Gutenberg: Building Our Custom Card Block