WordPress Logo カスタム投稿タイプを表示するダイナミックブロックの作成

設定されているカスタム投稿タイプを検出して表示するダイナミックブロックの作り方についての覚書です。

カスタムタクソノミー(ターム)も設定されていれば検出して、インスペクターパネルで選択できるようにし、表示する投稿をフィルターすることができます。

また、必要に応じて投稿や固定ページも対象にすることもできます。

この時点での WordPress のバージョンは 6.7.2、@wordpress/create-block のバージョンは 4.61.0 です。以下はローカル環境と Node(NPM)がインストールされていることを前提にしています。

更新日:2025年03月23日

作成日:2025年02月25日

カスタム投稿タイプ

以下では、すでに何らかの方法で少なくとも1つのカスタム投稿タイプが登録されてることを前提にしています。また、カスタム投稿タイプの投稿(記事)が作成されている必要があります。

カスタム投稿タイプは register_post_type() を使って登録することができますが、Advanced Custom Fields などのプラグインを使って登録することもできます。

この例では、Advanced Custom Fields を使ってカスタム投稿タイプを登録しています。

カスタム投稿タイプの投稿データを取得して、タイトルや抜粋、アイキャッチ画像を表示するので、カスタム投稿タイプの登録では、タイトルや抜粋、アイキャッチ画像が有効になっている必要があります。

以下はこの例で使用するカスタム投稿タイプ News の登録例です。

REST API を使用するので、show_in_rest が true になっている必要があり、supports ではタイトルや抜粋、アイキャッチ画像が有効になっています。

add_action('init', function () {
  register_post_type(
    'news',  // カスタム投稿タイプのスラッグ
    array(
      'labels' => array(
        'name' => 'News',
        'singular_name' => 'News',
        'menu_name' => 'News',
        'all_items' => 'News 一覧',
        'edit_item' => 'News を編集',

        //・・・中略・・・
      ),
      'public' => true,
      'show_in_rest' => true,  // REST API で使用可能にする
      'menu_icon' => 'dashicons-admin-post',
      'supports' => array(
        0 => 'title',  // タイトル
        1 => 'editor',  // 本文(とその編集機能)
        2 => 'excerpt',  // 抜粋
        3 => 'thumbnail',  // アイキャッチ画像
        4 => 'custom-fields',  // カスタムフィールド
      ),
      'delete_with_user' => false,
    )
  );
});

Advanced Custom Fields の場合、投稿タイプの設定画面の「高度な設定」の「全般」タブでタイトルや抜粋などが有効になっているかを確認できます。抜粋はデフォルトでは無効になっているようなので有効にします。

REST API で使用可能かどうかは REST API タブで確認できます(デフォルトは REST API で使用可能)。

また、実際の register_post_type() を使ったコードを確認するには、「ツール」のページで確認したい投稿タイプを選択して「PHPを生成」をクリックします。

例えば、以下のようにカスタム投稿タイプの登録のコードが表示されます。

関連ページ

Create Block ツールでひな型を作成

Create Block ツールの create-block コマンドを使って、ブロックのひな型と開発環境を構築します。

ターミナルでプラグインディレクトリ(wp-content/plugins)に移動して以下を実行します。

ダイナミックブロックを作成するので、--variant=dynamic オプションを指定します。

% npx @wordpress/create-block@latest --variant=dynamic --namespace wdl-block my-custom-posts

この例では --namespace オプション(ブロック名の内部名前空間)に wdl-block、slug(ひな形ファイルの出力先)に my-custom-posts を指定していますが、必要に応じて適宜変更します。

上記を実行すると、slug に指定した名前のディレクトリ my-custom-posts が作成されます。

npm start 開発を開始

ターミナルで作成したブロックのディレクトリ(my-custom-posts)に移動して npm start を実行し開発を開始します。これにより、src ディレクトリ内のファイルを変更して保存すると自動的にビルドが実行され、待機してファイルの変更を監視します。

% cd my-custom-posts
% npm start

開発を停止(npm start コマンドを終了)するには control + c を押します。再開するには再度 npm start を実行します。

プラグインを有効化

管理画面のプラグインページで作成したブロックのプラグイン(My Custom Posts)を有効化します。

新規に投稿を作成し、作成したブロックを挿入するとエディターは以下のように表示されます。

上記のエディターでの表示は、create-block により自動的に生成された edit.js によるものです(コメント部分は削除)。

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';

export default function Edit() {
  return (
    <p { ...useBlockProps() }>
      { __(
        'My Custom Posts – hello from the editor!',
        'my-custom-posts'
      ) }
    </p>
  );
}

フロントエンド側は、例えば以下のように表示されます。

上記フロントエンドの表示は create-block により自動的に生成された render.php によるものです。

<?php
/**
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */
?>
<p <?php echo get_block_wrapper_attributes(); ?>>
  <?php esc_html_e( 'My Custom Posts – hello from a dynamic block!', 'my-custom-posts' ); ?>
</p>

カスタム投稿タイプをエディターに表示

カスタム投稿タイプのスラッグ(slug)を指定して、エディターにその一覧をレンダリングする例です。

edit.js を以下のように書き換えます。

Edit() 関数(コンポーネント)で、useSelect フックのコールバック内で select 関数を使って core ストアにアクセスして getEntityRecords セレクターで投稿データを取得します。

通常の投稿の一覧を取得してエディターに表示するのと基本的に同じですが、getEntityRecords のパラメータで postType とカスタム投稿タイプのスラッグ(以下では news)を指定します。

この例では投稿のアイキャッチ画像とタイトル及び抜粋が設定されていればそれらを表示します。

アイキャッチ画像は medium(中)サイズの画像を取得しています。

タイトルと抜粋は、独自に定義した関数 getTextContent で取得した投稿データの title.rendered と excerpt.rendered を HTML からテキストに変換してレンダリングしています。

また、エディター内で各投稿のリンクをクリックすると、その編集画面内にリンク先の投稿が表示されてしまうので、この例ではクリックベントのハンドラー preventRedirect を定義して、リンクがクリックされたらアラートを表示してリダイレクトを防止しています。

import { __ } from "@wordpress/i18n";
import { useBlockProps } from "@wordpress/block-editor";
// @wordpress/data から useSelect をインポート
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit() {
  // useSelect フックを使用
  const posts = useSelect(
    (select) =>
      // select 関数で core ストアにアクセスして getEntityRecords で postType が news の投稿データを取得
      select("core").getEntityRecords("postType", "news", {
        per_page: 5, // 5 件の投稿を取得
        _embed: 'wp:featuredmedia', // アイキャッチ画像のデータも取得
      }),
    [],
  );

  // 要素に指定するクラス名の共通部分
  const pre = 'wp-block-wdl-block-my-custom-posts__';

  // HTML からテキストを取得する独自関数 getTextContent を定義
  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  // エディター内のリンクを無効にするイベントハンドラー
  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  }

  return (
    <div {...useBlockProps()}>
      <div className={`${pre}post-items`}>
        {Array.isArray(posts) && posts.length === 0 && (
          <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
        )}
        {posts &&
          posts.map((post) => {
            // アイキャッチ画像の URL
            const featuredMediaUrl = post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
            // アイキャッチ画像の代替テキスト
            const altText = post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

            return (
              <div key={post.id} className={`${pre}post-item`}>
                {featuredMediaUrl && (
                  <div className={`${pre}featured-image`}>
                    <a href={post.link} onClick={ preventRedirect}>
                      <img src={featuredMediaUrl} alt={altText} />
                    </a>
                  </div>
                )}
                {post.title?.rendered && (
                  <h3 className={`${pre}post-title`}>
                    <a href={post.link} onClick={ preventRedirect}>
                      {getTextContent(post.title.rendered)}
                    </a>
                  </h3>
                )}
                {post.excerpt?.rendered && (
                  <p className={`${pre}post-excerpt`}>
                    {getTextContent(post.excerpt.rendered)}
                  </p>
                )}
              </div>
            );
          })}
      </div>
    </div>
  );
}

例えば、以下のようにスラッグに指定したカスタム投稿タイプの投稿のアイキャッチ画像とタイトル及び抜粋が5件表示されます。

QueryControls を追加

投稿の表示件数や並び順、並び替え基準を設定できるように QueryControls を追加します。

block.json

表示件数、並び順、並び替え基準の値を保存するため、attributes プロパティに以下の属性を追加します。

  • numberOfItems: 表示する投稿数
  • order: 並び順
  • orderBy: 並び替え基準
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl-block/my-custom-posts",
  "version": "0.1.0",
  "title": "My Custom Posts",
  "category": "widgets",
  "icon": "smiley",
  "description": "Example block scaffolded with Create Block tool.",
  "example": {},
  "supports": {
    "html": false
  },
  "attributes": {
    "numberOfItems": {
      "type": "number",
      "default": 5
    },
    "order": {
      "type": "string",
      "default": "desc"
    },
    "orderBy": {
      "type": "string",
      "default": "date"
    }
  },
  "textdomain": "my-custom-posts",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

edit.js

edit.js を以下のように書き換えます。

インスペクターパネルを表示し、QueryControls を配置するため、InspectorControls や PanelBody, QueryControls コンポーネントをインポートします。

Edit 関数の引数で props から attributes と setAttributes を分割代入で受け取り、Edit 関数内で attributes から属性 numberOfItems, order, orderBy を分割代入で取得します。

そして属性を使って getEntityRecords のクエリパラメータの表示件数、並び順、並び替え基準を設定し、useSelect の依存配列に抽出条件の属性を追加します。

Edit 関数の return ステートメントに InspectorControls、PanelBody、QueryControls を追加し、return する JSX 全体をフラグメント (<>〜</>) で囲みます。

QueryControls では minItems と maxItems で表示件数の最小値と最大値を設定しています。

import { __ } from "@wordpress/i18n";
// InspectorControls のインポートを追加
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
// PanelBody, QueryControls をインポート
import { PanelBody, QueryControls } from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

// Edit 関数の引数で props から attributes と属性を更新する関数 setAttributes を分割代入で受け取る
export default function Edit({ attributes, setAttributes }) {
  // attributes から属性 numberOfItems, order, orderBy を分割代入で変数に代入
  const { numberOfItems, order, orderBy } = attributes;

  const posts = useSelect(
    (select) =>
      select("core").getEntityRecords("postType", "news", {
        per_page: numberOfItems, // 表示件数
        _embed: "wp:featuredmedia",
        order, // 並び順
        orderby: orderBy, // 並び替え基準
      }),
    // 依存配列に抽出条件の属性を追加
    [numberOfItems, order, orderBy],
  );

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        <div className={`${pre}post-items`}>
          {Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}

以下のようなコントロールがサイドバーに表示され、並び順や表示数を設定できるようになります。

基本的なダイナミックブロックの作成については以下を御覧ください。

投稿タイプの選択

前述の例の場合、カスタム投稿タイプのスラッグをハードコードしていますが、ユーザーが投稿タイプをプルダウンメニューで選択できるようにします。

block.json

ユーザーが選択した投稿タイプを保存する属性 myPostType を block.json の attributes プロパティに追加します。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl-block/my-custom-posts",
  "version": "0.1.0",
  "title": "My Custom Posts",
  "category": "widgets",
  "icon": "smiley",
  "description": "Example block scaffolded with Create Block tool.",
  "example": {},
  "supports": {
    "html": false
  },
  "attributes": {
    "myPostType": {
      "type": "string",
      "default": ""
    },
    "numberOfItems": {
      "type": "number",
      "default": 5
    },
    "order": {
      "type": "string",
      "default": "desc"
    },
    "orderBy": {
      "type": "string",
      "default": "date"
    }
  },
  "textdomain": "my-custom-posts",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

edit.js

WordPress に登録されている投稿タイプのデータは、core ストアの getPostTypes セレクターを使って取得することができます。

投稿タイプを選択するプルダウンメニューは SelectControl コンポーネントを使って作成します。

edit.js を以下のように書き換えます。

attributes から属性 myPostType を分割代入で変数に代入します(10行目)。myPostType の値は SelectControl コンポーネントで更新されます。

getEntityRecords() の第2引数にはスラッグの代わりに myPostType を指定し、選択された投稿タイプの投稿データを取得するようにします(17行目)。

useSelect フックで、getPostTypes() に { per_page: -1 } を指定して投稿タイプのデータを全て取得して変数 postTypes に格納します(24行目)。

myPostType の値が変更されたら useSelect が再評価(再実行)されるように依存配列に myPostType を指定します(27行目)。

31-45行目は、SelectControl の options に指定する投稿タイプのリストを作成しています(後述)。

コンテンツのレンダリングでは、myPostType が選択されていない場合(初期状態など)は「Select a post type」と表示し、myPostType が設定されている場合は、投稿の取得が完了するまでは「Loading...」と表示します(87-88行目)。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
// SelectControl を追加でインポート
import { PanelBody, QueryControls, SelectControl } from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit({ attributes, setAttributes }) {
  // 属性 myPostType を追加で取得
  const { numberOfItems, order, orderBy, myPostType } = attributes;

  // 投稿データに加えて投稿タイプのデータ postTypes も取得
  const { posts, postTypes } = useSelect(
    (select) => {
      return {
        // 投稿タイプに myPostType を指定
        posts: select("core").getEntityRecords("postType", myPostType, {
          per_page: numberOfItems,
          _embed: "wp:featuredmedia",
          order,
          orderby: orderBy,
        }),
        // 登録されている投稿タイプを全て取得
        postTypes: select("core").getPostTypes({ per_page: -1 }),
      };
    },
    [numberOfItems, order, orderBy, myPostType], // 依存配列に myPostType を追加
  );

  // SelectControl コンポーネント用にオプションを生成
  const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" }, // デフォルトオプション
      ...(postTypes
        ? postTypes
            // 投稿、固定ページ、カスタム投稿タイプのみを取得
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                type.slug !== "attachment",
            )
            // { label: 投稿タイプ名, value: スラッグ } 形式に変換
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
          <SelectControl
            label={__("Post Type", "my-custom-posts")}
            value={myPostType}
            options={options}
            onChange={(value) => setAttributes({ myPostType: value })}
          />
        </PanelBody>
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        {!myPostType && <p>{__("Select a post type.", "my-custom-posts")}</p>}
        {myPostType && !posts ? <p>{__("Loading...", "my-custom-posts")}</p> : null}
        <div className={`${pre}post-items`}>
          {myPostType && Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}

新たにブロックを挿入すると、インスペクターパネルに投稿タイプを選択するプルダウンが表示され、投稿タイプを選択できるようになります。

投稿タイプのデータ

getPostTypes() の戻り値(postTypes)は slug や name、capabilities などのプロパティを持つ投稿タイプのオブジェクトの配列です。

以下は投稿タイプが投稿(post)の投稿タイプオブジェクトの例です。

{
  "capabilities": {
    "edit_post": "edit_post",
    "read_post": "read_post",
    "delete_post": "delete_post",
    "edit_posts": "edit_posts",
    "edit_others_posts": "edit_others_posts",
    "delete_posts": "delete_posts",
    "publish_posts": "publish_posts",
    "read_private_posts": "read_private_posts",
    "read": "read",
    "delete_private_posts": "delete_private_posts",
    "delete_published_posts": "delete_published_posts",
    "delete_others_posts": "delete_others_posts",
    "edit_private_posts": "edit_private_posts",
    "edit_published_posts": "edit_published_posts",
    "create_posts": "edit_posts"
  },
  "description": "",
  "hierarchical": false,
  "has_archive": false,
  "visibility": {
    "show_in_nav_menus": true,
    "show_ui": true
  },
  "viewable": true,
  "labels": {
    "name": "投稿",
    "singular_name": "投稿",
    "add_new": "新規追加",
    "add_new_item": "新規投稿を追加",
    "edit_item": "投稿を編集",
    "new_item": "新規投稿",
    "view_item": "投稿を表示",
    "view_items": "投稿一覧を表示",
    "search_items": "投稿を検索",
    "not_found": "投稿が見つかりませんでした。",
    "not_found_in_trash": "ゴミ箱に投稿はありません。",
    "parent_item_colon": null,
    "all_items": "投稿一覧",
    "archives": "投稿アーカイブ",
    "attributes": "投稿の属性",
    "insert_into_item": "投稿に挿入",
    "uploaded_to_this_item": "この投稿へのアップロード",
    "featured_image": "アイキャッチ画像",
    "set_featured_image": "アイキャッチ画像を設定",
    "remove_featured_image": "アイキャッチ画像を削除",
    "use_featured_image": "アイキャッチ画像として使用",
    "filter_items_list": "投稿一覧を絞り込む",
    "filter_by_date": "日付で絞り込む",
    "items_list_navigation": "投稿リストナビゲーション",
    "items_list": "投稿リスト",
    "item_published": "投稿を公開しました。",
    "item_published_privately": "投稿を限定公開しました。",
    "item_reverted_to_draft": "投稿を下書きに戻しました。",
    "item_trashed": "投稿をゴミ箱に移動しました。",
    "item_scheduled": "投稿を予約しました。",
    "item_updated": "投稿を更新しました。",
    "item_link": "投稿リンク",
    "item_link_description": "投稿へのリンク。",
    "menu_name": "投稿",
    "name_admin_bar": "投稿"
  },
  "name": "投稿",
  "slug": "post",
  "icon": "dashicons-admin-post",
  "supports": {
    "title": true,
    "editor": true,
    "author": true,
    "thumbnail": true,
    "excerpt": true,
    "trackbacks": true,
    "custom-fields": true,
    "comments": true,
    "revisions": true,
    "post-formats": true,
    "autosave": true
  },
  "taxonomies": [
    "category",
    "post_tag"
  ],
  "rest_base": "posts",
  "rest_namespace": "wp/v2",
  "template": [],
  "template_lock": false,
  "_links": {
    "collection": [
      {
        "href": "http://localhost/wp-sample/wp-json/wp/v2/types"
      }
    ],
    "wp:items": [
      {
        "href": "http://localhost/wp-sample/wp-json/wp/v2/posts"
      }
    ],
    "curies": [
      {
        "name": "wp",
        "href": "https://api.w.org/{rel}",
        "templated": true
      }
    ]
  }
}
SelectControl のオプションの作成

以下は SelectControl コンポーネントの options に指定する投稿タイプのリストの作成部分の抜粋です。

options は label と value プロパティを持つオブジェクトの配列になります。

最初にデフォルトオプションのオブジェクトを作成しておきます。

投稿タイプのデータ(postTypes)が取得できていれば、filter() を使って postTypes から投稿、固定ページ、カスタム投稿タイプのみを抽出します。

そして、map() で label が投稿タイプ名(type.name)、value がスラッグ(type.slug)のオブジェクトの配列に変換し、スプレッド構文で options 配列に展開しています。

// SelectControl コンポーネント用にオプションを生成
const options = [
  { label: __("Select a Post Type", "my-custom-posts"), value: "" }, // デフォルトオプション
    ...(postTypes
      ? postTypes
          // 投稿、固定ページ、カスタム投稿タイプのみを取得
          .filter(
            (type) =>
              type.viewable &&  // フロントエンドで表示可能な投稿タイプ
              type.capabilities.create_posts !== undefined && // create_posts 権限があるもの
              type.slug !== "attachment", // メディアを除外
          )
          // { label: 投稿タイプ名, value: スラッグ } 形式に変換
          .map((type) => ({ label: type.name, value: type.slug }))
      : []),
  ];

この例の場合、2つのカスタム投稿タイプ(news、works)を作成しているので、上記 options は以下のようなオブジェクトの配列になります。

[
  {
    "label": "Select a Post Type",
    "value": ""
  },
  {
    "label": "投稿",
    "value": "post"
  },
  {
    "label": "固定ページ",
    "value": "page"
  },
  {
    "label": "News",
    "value": "news"
  },
  {
    "label": "Works",
    "value": "works"
  }
]

filter() を適用しない場合、options は以下のようなオブジェクトの配列になります。

[
  {
    "label": "Select a Post Type",
    "value": ""
  },
  {
    "label": "投稿",
    "value": "post"
  },
  {
    "label": "固定ページ",
    "value": "page"
  },
  {
    "label": "メディア",
    "value": "attachment"
  },
  {
    "label": "ナビゲーションメニューの項目",
    "value": "nav_menu_item"
  },
  {
    "label": "パターン",
    "value": "wp_block"
  },
  {
    "label": "テンプレート",
    "value": "wp_template"
  },
  {
    "label": "テンプレートパーツ",
    "value": "wp_template_part"
  },
  {
    "label": "グローバルスタイル",
    "value": "wp_global_styles"
  },
  {
    "label": "ナビゲーションメニュー",
    "value": "wp_navigation"
  },
  {
    "label": "フォントファミリー",
    "value": "wp_font_family"
  },
  {
    "label": "フォントフェイス",
    "value": "wp_font_face"
  },
  {
    "label": "News",
    "value": "news"
  },
  {
    "label": "Works",
    "value": "works"
  }
]
投稿タイプを判別するための条件

getPostTypes() は、register_post_type() で登録されたすべての投稿タイプを取得するため、wp_navigation や wp_block などエディタの内部機能用の投稿タイプも含まれます。

上記の filter() で判定している条件は投稿タイプデータの以下のプロパティを使っています。

  1. viewable: true

    • true :フロントエンドで表示可能な投稿タイプ。
    • false : 管理画面専用の投稿タイプ。
  2. capabilities.create_posts

    • 投稿(記事)を作成できるかどうかを確認。
    • 通常のカスタム投稿タイプには create_posts 権限がある。
    • wp_navigation や wp_block などの特殊な投稿タイプには create_posts がないため除外可能。
    • オブジェクトなので単純な真偽値チェックではなく undefined でないかをチェック。
  3. slug:投稿タイプのスラッグ

カスタム投稿タイプのみを選択

投稿や固定ページは除外して、カスタム投稿タイプのみから選択するようにします。

selectControl の options にカスタム投稿タイプだけを取得するには以下のように書き換えます。

includes() を使って slug が post、page、attachment の投稿タイプを除外しています(type.slug !== 'post' && type.slug !== 'page' && type.slug !== 'attachment'; と同じことです)。

const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" },
      ...(postTypes
        ? postTypes
            // カスタム投稿タイプのみを取得
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                ![ 'post', 'page', 'attachment' ].includes( type.slug ) , // 投稿、固定ページ、メディアを除外
            )
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];
edit.js

以下は edit.js のコード全体です。SelectControl の label を Custom Post Type に変更しています。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { PanelBody, QueryControls, SelectControl } from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit({ attributes, setAttributes }) {
  const { numberOfItems, order, orderBy, myPostType } = attributes;

  const { posts, postTypes } = useSelect(
    (select) => {
      return {
        posts: select("core").getEntityRecords("postType", myPostType, {
          per_page: numberOfItems,
          _embed: "wp:featuredmedia",
          order,
          orderby: orderBy,
        }),
        postTypes: select("core").getPostTypes({ per_page: -1 }),
      };
    },
    [numberOfItems, order, orderBy, myPostType],
  );

  // カスタム投稿タイプのみのリストを作成
  const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" },
      ...(postTypes
        ? postTypes
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                ![ 'post', 'page', 'attachment' ].includes( type.slug ) , // 投稿、固定ページ、メディアを除外
            )
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
          <SelectControl
            label={__("Custom Post Type", "my-custom-posts")}
            value={myPostType}
            options={options}
            onChange={(value) => setAttributes({ myPostType: value })}
          />
        </PanelBody>
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        {!myPostType && <p>{__("Select a post type.", "my-custom-posts")}</p>}
        {myPostType && !posts ? <p>{__("Loading...", "my-custom-posts")}</p> : null}
        <div className={`${pre}post-items`}>
          {myPostType && Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}

上記のように変更すると、インスペクターパネルのプルダウンには、デフォルト値とカスタム投稿タイプのみが表示されます。

フロントエンドにコンテンツを表示

フロントエンド側の表示は block.json の render プロパティに指定されている render.php に記述します。

edit.js のエディター側でのレンダリングと同じ構造の HTML をフロントエンドでレンダリングするように render.php でコーディングします。

render.php ではブロックの属性の配列 $attributes を自動的に受け取ります。

render.php

以下が render.php です。

エディター側で選択した条件の投稿を取得するためのクエリの引数を設定します。

ブロックの属性は $attributes 配列で受け取ることができ、例えば、表示する投稿数の属性 numberOfItems は $attributes['numberOfItems'], でアクセスできます。

投稿タイプ以外のクエリの引数を変数 $args に設定し、myPostType が 許可された値かどうかを検証し、問題がなければクエリの引数に投稿タイプを追加し、WP_Query で投稿を取得します。

そして、投稿リストのコンテナを作成し、ループの中で投稿アイテムのコンテナを作成してその中にマークアップを追加していきます(詳細はコメントを参照ください)。

出力する値は、esc_attr()esc_url()esc_html() などを使って適切にエスケープします。タイトルや抜粋は HTML を含む可能性がるので、以下では wp_strip_all_tags() を使ってタグを削除しています。

WP_Query を使ったサブループは the_post() を使うのでグローバル変数 $post を書き換えるため、最後に wp_reset_post_data() を実行して書き換えられた $post の値を元に戻します(122行目)。

最終的なマークアップを出力する際に、念の為、適切なタグを許可する関数 wp_kses() を使ってコンテンツをサニタイズして、printf() で出力しています。

その際に、wp_kses_allowed_html() でデフォルトの許可タグを取得し、img の srcset 属性と sizes 属性を許可タグのリストに追加して(128-132行目)、wp_kses() の第2引数に指定しています。

サニタイズに wp_kses_post() を使うと、アイキャッチ画像の img 要素の srcset や sizes 属性が削除されてしまいます。

<?php

// クエリの引数を設定
$args = array(
  'posts_per_page'      => $attributes['numberOfItems'], // 表示する投稿数
  'post_status'         => 'publish', // 公開済みの投稿のみ取得
  'order'               => $attributes['order'], // 並び順(昇順・降順)
  'orderby'             => $attributes['orderBy'], // 並び替えの基準(例: 日付, タイトル)
  'ignore_sticky_posts' => true, // スティッキーポスト(先頭に固定表示された投稿)を無視
  'no_found_rows'       => true, // クエリの総投稿数を取得しない(パフォーマンス向上)
);

// 許可された投稿タイプのリストを取得
$allowed_post_types = get_post_types(['public' => true]);

// myPostType が許可されたものかチェック
if (!isset($attributes['myPostType']) || !in_array($attributes['myPostType'], $allowed_post_types, true)) {
    echo '<p>' . esc_html__('Invalid post type.', 'my-custom-posts') . '</p>';
    // myPostType が適切でない場合は処理を中断(無駄なクエリを実行しない)
    return;
}

// クエリの引数に投稿タイプを追加
$args['post_type'] = $attributes['myPostType'];

// WP_Query で投稿を取得
$query = new WP_Query($args);

// CSS クラスのプレフィックスを設定
$pre = 'wp-block-wdl-block-my-custom-posts__';

// 投稿リストのHTMLを格納する変数
$post_items_markup = '';

// 投稿リストのコンテナを作成
$post_items_markup .= sprintf(
  '<div class="%1$spost-items">',
  esc_attr($pre)
);

// 投稿が存在すれば
if ($query->have_posts()) {
  // 投稿のループ処理
  while ($query->have_posts()) {
    $query->the_post();
    $post_link = esc_url(get_permalink());  // 投稿のURL
    $title     = wp_strip_all_tags(get_the_title());  // 投稿のタイトル(HTMLタグを除去)

    // 投稿アイテムのコンテナを作成
    $post_items_markup .= sprintf(
      '<div class="%1$spost-item">',
      esc_attr($pre)
    );

    // アイキャッチ画像が設定されていれば表示
    if (has_post_thumbnail()) {
      // alt属性の値を取得
      $thumbnail_alt_value = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true);

      // alt属性が空の場合、投稿タイトルを代わりに設定
      if (!$thumbnail_alt_value) {
        $thumbnail_alt_value = $title;
      }

      // アイキャッチ画像のHTMLを取得
      $featured_image = get_the_post_thumbnail(
        get_the_ID(),
        'medium',  // 画像サイズ
        array(
          'alt'   => esc_attr($thumbnail_alt_value),  // alt属性の設定
        )
      );

      // アイキャッチ画像にリンクを追加
      $featured_image = sprintf(
        '<a href="%1$s" aria-label="%2$s">%3$s</a>',
        esc_url($post_link),
        esc_attr($title), // アクセシビリティのためのラベル
        $featured_image
      );

      // 画像のHTMLを追加
      $post_items_markup .= sprintf(
        '<div class="%1$sfeatured-image">%2$s</div>',
        esc_attr($pre),
        $featured_image
      );
    }

    // 投稿タイトルのHTMLを追加
    $post_items_markup .= sprintf(
      '<%1$s class="%2$spost-title"><a href="%3$s">%4$s</a></%1$s>',
      'h3',
      esc_attr($pre),
      esc_url($post_link),
      $title
    );

    // 抜粋を表示
    $post_items_markup .= sprintf(
      '<p class="%1$spost-excerpt">%2$s</p>',
      esc_attr($pre),
      wp_strip_all_tags(get_the_excerpt())
    );

    // 投稿アイテムの終了タグ
    $post_items_markup .= "</div>\n";
  }
} else {
  // 投稿がない場合のメッセージ
  $post_items_markup .= sprintf(
    '<p class="%1$sno-post">%2$s</p>',
    esc_attr($pre),
    esc_html__('No posts to display.', 'my-custom-posts')
  );
}

// 投稿リストの終了タグ
$post_items_markup .= "</div>\n";

// 投稿データをリセット
wp_reset_postdata();

// ブロックのラッパー属性を取得
$wrapper_attributes = get_block_wrapper_attributes();

// wp_kses() に指定するデフォルトの許可タグを取得
$allowed_tags = wp_kses_allowed_html('post');
// 許可タグのリストに img の srcset 属性を追加
$allowed_tags['img']['srcset'] = true;
// 許可タグのリストに img の sizes 属性を追加
$allowed_tags['img']['sizes'] = true;

// 最終的なHTMLを出力
printf(
  '<div %1$s>%2$s</div>',
  $wrapper_attributes,
  // セキュリティ対策としてHTMLのコンテンツをサニタイズ(無害化)
  wp_kses($post_items_markup, $allowed_tags)
);

投稿タイプのデフォルトを post にする

この例の場合は、表示するのはカスタム投稿タイプのみですが、例えば、カスタム投稿タイプ以外にも通常の投稿(post)を表示する場合などでは、投稿タイプ(myPostType)の検証で以下のようにデフォルトを post にする方法もあります。

<?php

$args = array(
  'posts_per_page'      => $attributes['numberOfItems'],
  'post_status'         => 'publish',
  'order'               => $attributes['order'],
  'orderby'             => $attributes['orderBy'],
  'ignore_sticky_posts' => true,
  'no_found_rows'       => true,
);

// 投稿タイプ
$custom_post_type = $attributes['myPostType'];
// 登録済みの投稿タイプ
$allowed_post_types = get_post_types(['public' => true]);

// $attributes['myPostType'] の値を登録済みの投稿タイプと照合
if (!in_array($custom_post_type, $allowed_post_types, true)) {
  // 投稿タイプの値が不正(存在しない投稿タイプや空文字)だった場合のデフォルトを 'post' に
  $custom_post_type = 'post';
}

// クエリの引数に投稿タイプを追加
$args['post_type'] = $custom_post_type;

// WP_Query で投稿を取得
$query = new WP_Query($args);

// 以降は同じなので省略

タクソノミーとタームの選択

選択されたカスタム投稿タイプにカスタムタクソノミーが設定されている場合は、選択されたカスタムタクソノミーに登録されているタームを選択して投稿を絞り込めるようにします。

block.json

ユーザーが選択したカスタムタクソノミーとタームを保存する属性 selectedTaxonomy と selectedTerm を block.json の attributes プロパティに追加します。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl-block/my-custom-posts",
  "version": "0.1.0",
  "title": "My Custom Posts",
  "category": "widgets",
  "icon": "smiley",
  "description": "Example block scaffolded with Create Block tool.",
  "example": {},
  "supports": {
    "html": false
  },
  "attributes": {
    "myPostType": {
      "type": "string",
      "default": ""
    },
    "selectedTaxonomy": {
      "type": "string",
      "default": ""
    },
    "selectedTerm": {
      "type": "string",
      "default": ""
    },
    "numberOfItems": {
      "type": "number",
      "default": 5
    },
    "order": {
      "type": "string",
      "default": "desc"
    },
    "orderBy": {
      "type": "string",
      "default": "date"
    }
  },
  "textdomain": "my-custom-posts",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

edit.js

edit.js を以下のように書き換えます。

全てのタクソノミーを取得し、各タクソノミーを調べて選択されたカスタム投稿タイプに関連するタクソノミーを抽出します。そしてタクソノミーが選択されたら、そのタームを取得して表示します。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import { PanelBody, QueryControls, SelectControl } from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit({ attributes, setAttributes }) {
  // selectedTaxonomy, selectedTerm を追加
  const { numberOfItems, order, orderBy, myPostType, selectedTaxonomy, selectedTerm } = attributes;

  // 関連タクソノミー・ターム(taxonomies, terms)を追加で取得
  const { posts, postTypes, taxonomies, terms } = useSelect(
    (select) => {
      // select("core") を複数回使用するので変数 core に格納
      const core = select("core");
      // 全てのタクソノミーを取得
      const allTaxonomies = core.getTaxonomies();
      // 選択されたカスタム投稿タイプに関連するタクソノミーを取得
      const relatedTaxonomies = allTaxonomies
        ? allTaxonomies.filter((taxonomy) =>
            taxonomy.types.includes(myPostType),
          )
        : [];

      return {
        posts: core.getEntityRecords("postType", myPostType, {
          per_page: numberOfItems,
          _embed: "wp:featuredmedia",
          order,
          orderby: orderBy,
          // selectedTaxonomy と selectedTerm が選択されている場合にフィルタリング
          ...(selectedTaxonomy && selectedTerm
            ? { [`${selectedTaxonomy}`]: selectedTerm }
            : {}),
        }),
        postTypes: core.getPostTypes({ per_page: -1 }),
        taxonomies: relatedTaxonomies,
        // 選択されたタクソノミーに属するすべてのタームを取得
        terms: selectedTaxonomy
          ? core.getEntityRecords("taxonomy", selectedTaxonomy, {
              per_page: -1,
            })
          : [],
      };
    },
    [numberOfItems, order, orderBy, myPostType, selectedTaxonomy, selectedTerm],
  );

  const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" },
      ...(postTypes
        ? postTypes
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                ![ 'post', 'page', 'attachment' ].includes( type.slug ),
            )
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];

  // タクソノミー選択オプション
  const taxonomyOptions = [
    { label: __("Select a Taxonomy", "my-custom-posts"), value: "" },
    ...(taxonomies
      ? taxonomies.map((tax) => ({ label: tax.name, value: tax.slug }))
      : []),
  ];

  // ターム選択オプション
  const termOptions = [
    { label: __("Select a Term", "my-custom-posts"), value: "" },
    ...(terms
      ? terms.map((term) => ({ label: term.name, value: term.id }))
      : []),
  ];

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
          <SelectControl
            label={__("Custom Post Type", "my-custom-posts")}
            value={myPostType}
            options={options}
            onChange={(value) => setAttributes({ myPostType: value, selectedTaxonomy: "", selectedTerm: "" })}
          />
        </PanelBody>
        {myPostType && taxonomies.length > 0 && (
          <PanelBody title={__("Taxonomy Settings", "my-custom-posts")}>
            <SelectControl
              label={__("Select a Taxonomy", "my-custom-posts")}
              value={selectedTaxonomy}
              options={taxonomyOptions}
              onChange={(value) =>
                setAttributes({ selectedTaxonomy: value, selectedTerm: "" })
              }
            />
            {selectedTaxonomy && (
              <SelectControl
                label={__("Select a Term", "my-custom-posts")}
                value={selectedTerm}
                options={termOptions}
                onChange={(value) => setAttributes({ selectedTerm: value })}
              />
            )}
          </PanelBody>
        )}
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        {!myPostType && <p>{__("Select a post type.", "my-custom-posts")}</p>}
        {myPostType && !posts ? <p>{__("Loading...", "my-custom-posts")}</p> : null}
        <div className={`${pre}post-items`}>
          {myPostType && Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}
タクソノミーの取得

useSelect フック内の以下の部分は選択されたカスタム投稿タイプに関連するタクソノミーを取得する処理を行っています。

allTaxonomies は getTaxonomies() を使って取得したサイト内のすべてのタクソノミーの配列です。

filter() メソッドを使って、allTaxonomies 配列の中から taxonomy.types.includes(myPostType) の条件を満たすタクソノミーのみを取得しています。taxonomy.types は配列なので includes() でその配列に選択されたカスタム投稿タイプが含まれるものを抽出します。

allTaxonomies が null や undefined の場合、 .filter() を実行するとエラーになるため、? を使って存在チェックを行い、allTaxonomies が null だった場合は、空の配列 [] を返します。

// 全てのタクソノミーを取得
const allTaxonomies = core.getTaxonomies();
// 選択されたカスタム投稿タイプに関連するタクソノミーを取得
const relatedTaxonomies = allTaxonomies
  ? allTaxonomies.filter((taxonomy) =>
      // types プロパティ(配列)に選択されたカスタム投稿タイプが含まれるものを抽出
      taxonomy.types.includes(myPostType),
    )
  : [];
タームの取得

以下は選択されたタクソノミー(selectedTaxonomy)に属する全てのタームを取得する処理部分です。

selectedTaxonomy が 存在する場合、getEntityRecords() の第1引数に taxonomy を、第2引数に選択されたタクソノミー selectedTaxonomy を指定して、クエリパラメータで per_page: に -1 を指定して選択されたタクソノミーに属するすべてのタームを取得しています。

selectedTaxonomy が 空や null の場合は、エラーを防ぐために空の配列をセットします。

 // 選択されたタクソノミーに属するすべてのタームを取得
terms: selectedTaxonomy
  ? core.getEntityRecords("taxonomy", selectedTaxonomy, {
      per_page: -1,
    })
  : [],
タームに属する投稿を取得

以下はタームに属する投稿の取得部分の抜粋です。

selectedTaxonomy と selectedTerm の両方が選択されている場合は、スプレッド構文でクエリパラメータに { タクソノミースラッグ: タームID } を追加し、何も選択されていない場合は {} を展開し何も追加しません。

[`${selectedTaxonomy}`]算出プロパティ名を使ってオブジェクトのキーを動的に生成しています。※ プロパティ名(キー)を角括弧 [ ] で囲むことでプロパティ名に変数を使うことができます。

posts: core.getEntityRecords("postType", myPostType, {
  per_page: numberOfItems,
  _embed: "wp:featuredmedia",
  order,
  orderby: orderBy,
  // selectedTaxonomy と selectedTerm が選択されている場合にフィルタリング
  ...(selectedTaxonomy && selectedTerm
    ? { [`${selectedTaxonomy}`]: selectedTerm }
    : {}),
}),

例えば、selectedTaxonomy が news-category で selectedTerm が 5 であれば、{ news-category: 5 } のようなオブジェクトが生成され、クエリパラメータとして追加されて news-category の ID が 5 に紐づく投稿だけが取得されます。

GET /wp/v2/posts の Arguments(参考)

上記の{[`${selectedTaxonomy}`]: selectedTerm} のキーの部分は、WordPress REST API の GET /wp/v2/posts エンドポイントのカスタムタクソノミークエリパラメータに相当します(ドキュメントにそのようなパラメータは記載されていませんが)。

カスタム投稿タイプのエンドポイントは /wp/v2/{custom_post_type} になります。

WordPress REST API の GET /wp/v2/posts エンドポイントでは、以下のようなタクソノミー関連のクエリパラメータが利用可能です(custom_taxonomy はドキュメントに記載はありません)。

パラメータ 説明
categories カテゴリー ID(複数可, カンマ区切り)
tags タグ ID(複数可, カンマ区切り)
custom_taxonomy カスタムタクソノミーの ID(複数可, カンマ区切り)

例えば、https://xxxx/wp-json/wp/v2/posts?categories=5,10 のように指定すると、投稿(posts)のカテゴリー ID 5 または 10 に属する投稿が取得できます。

同様に、https://xxxx/wp-json/wp/v2/news?news-category=5 のように指定すると、カスタム投稿タイプ news の news-category ID 5 に属する投稿が取得できます。

カスタム投稿タイプの登録(register_post_type)で REST API が使用可能('show_in_rest' => true)になっている必要があります。

SelectControl のオプション

SelectControl に指定する options は label と value プロパティを持つオブジェクトの配列を作成します。

以下はタクソノミー選択の SelectControl に指定するオプションです。

taxonomies が true であれば、taxonomies はカスタム投稿タイプに関連するタクソノミーの配列なので、map() で name と slug プロパティを取得して、 label と value プロパティを持つオブジェクトの配列を作成しています。

// タクソノミー選択オプション
const taxonomyOptions = [
  { label: __("Select a Taxonomy", "my-custom-posts"), value: "" },
  ...(taxonomies
    ? taxonomies.map((tax) => ({ label: tax.name, value: tax.slug }))
    : []),
];

以下はターム選択の SelectControl に指定するオプションです。

terms が true であれば、terms は選択されたタクソノミーに属するすべてのタームの配列なので、map() で name と id プロパティを取得して、 label と value プロパティを持つオブジェクトの配列を作成しています。

// ターム選択オプション
const termOptions = [
  { label: __("Select a Term", "my-custom-posts"), value: "" },
  ...(terms
    ? terms.map((term) => ({ label: term.name, value: term.id }))
    : []),
];
SelectControl のレンダリング

タクソノミー選択とターム選択の SelectControl をインスペクターパネルに配置します。

この2つの SelectControl は myPostType && taxonomies.length > 0 &&(10行目)で選択されたカスタム投稿タイプにタクソノミーが存在する場合にタクソノミー選択を表示し、selectedTaxonomy &&(20行目)で選択されたタクソノミーが存在する場合にターム選択を表示します。

value には属性 selectedTaxonomy と selectedTerm をそれぞれ指定し、options には前項で説明したオプションを指定します。

また、カスタム投稿タイプの選択が変更されたらタクソノミーとタームの値をクリアし(7行目)、タクソノミー選択の値が変更されたら、タームの値をクリアします(17行目)。

<InspectorControls>
  <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
    <SelectControl
      label={__("Custom Post Type", "my-custom-posts")}
      value={myPostType}
      options={options}
      onChange={(value) => setAttributes({ myPostType: value, selectedTaxonomy: "", selectedTerm: "" })}
    />
  </PanelBody>
  {myPostType && taxonomies.length > 0 && (
    <PanelBody title={__("Taxonomy Settings", "my-custom-posts")}>
      <SelectControl
        label={__("Select a Taxonomy", "my-custom-posts")}
        value={selectedTaxonomy}
        options={taxonomyOptions}
        onChange={(value) =>
          setAttributes({ selectedTaxonomy: value, selectedTerm: "" })
        }
      />
      {selectedTaxonomy && (
        <SelectControl
          label={__("Select a Term", "my-custom-posts")}
          value={selectedTerm}
          options={termOptions}
          onChange={(value) => setAttributes({ selectedTerm: value })}
        />
      )}
    </PanelBody>
  )}
  ・・・中略・・・
</InspectorControls>

選択したカスタム投稿タイプにタクソノミーが設定されていれば、タクソノミー選択のプルダウンが表示されます。

選択したタクソノミーにタームが設定されていれば、ターム選択のプルダウンが表示され、選択したタームで表示する投稿を絞り込むことができます。

render.php

render.php を以下のように変更します。

$attributes['selectedTaxonomy'] と $attributes['selectedTerm'] が空でなければ、クエリに tax_query パラメータを追加します(22-30行目)。

terms の指定では、is_array() を使って配列と単一 ID の両方に対応し、intval() で数値化しています。

<?php

$args = array(
  'posts_per_page'      => $attributes['numberOfItems'],
  'post_status'         => 'publish',
  'order'               => $attributes['order'],
  'orderby'             => $attributes['orderBy'],
  'ignore_sticky_posts' => true,
  'no_found_rows'       => true,
);

$allowed_post_types = get_post_types(['public' => true]);

if (!isset($attributes['myPostType']) || !in_array($attributes['myPostType'], $allowed_post_types, true)) {
  echo '<p>' . esc_html__('Invalid post type.', 'my-custom-posts') . '</p>';
  return;
}

$args['post_type'] = $attributes['myPostType'];

// selectedTaxonomy と selectedTerm が空でなければクエリに tax_query パラメータを追加
if (!empty($attributes['selectedTaxonomy']) && !empty($attributes['selectedTerm'])) {
  $args['tax_query'] = array(
    array(
      'taxonomy' => $attributes['selectedTaxonomy'],
      'field' => 'term_id',
      // $attributes['selectedTerms'] が配列と単一 ID の両方に対応
      'terms'    => is_array($attributes['selectedTerm']) ? array_map('intval', $attributes['selectedTerm']) : intval($attributes['selectedTerm']),
    )
  );
}

$query = new WP_Query($args);

$pre = 'wp-block-wdl-block-my-custom-posts__';

$post_items_markup = '';

$post_items_markup .= sprintf(
  '<div class="%1$spost-items">',
  esc_attr($pre)
);

if ($query->have_posts()) {
  while ($query->have_posts()) {
    $query->the_post();
    $post_link = esc_url(get_permalink());
    $title     = wp_strip_all_tags(get_the_title());

    $post_items_markup .= sprintf(
      '<div class="%1$spost-item">',
      esc_attr($pre)
    );

    if (has_post_thumbnail()) {

      $thumbnail_alt_value = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true);

      if (!$thumbnail_alt_value) {
        $thumbnail_alt_value = $title;
      }

      $featured_image = get_the_post_thumbnail(
        get_the_ID(),
        'medium',
        array(
          'alt'   => esc_attr($thumbnail_alt_value),
        )
      );

      $featured_image = sprintf(
        '<a href="%1$s" aria-label="%2$s">%3$s</a>',
        esc_url($post_link),
        esc_attr($title),
        $featured_image
      );

      $post_items_markup .= sprintf(
        '<div class="%1$sfeatured-image">%2$s</div>',
        esc_attr($pre),
        $featured_image
      );
    }

    $post_items_markup .= sprintf(
      '<%1$s class="%2$spost-title"><a href="%3$s">%4$s</a></%1$s>',
      'h3',
      esc_attr($pre),
      esc_url($post_link),
      $title
    );

    $post_items_markup .= sprintf(
      '<p class="%1$spost-excerpt">%2$s</p>',
      esc_attr($pre),
      wp_strip_all_tags(get_the_excerpt())
    );

    $post_items_markup .= "</div>\n";
  }
} else {
  $post_items_markup .= sprintf(
    '<p class="%1$sno-post">%2$s</p>',
    esc_attr($pre),
    esc_html__('No posts to display.', 'my-custom-posts')
  );
}

$post_items_markup .= "</div>\n";

wp_reset_postdata();

$wrapper_attributes = get_block_wrapper_attributes();
$allowed_tags = wp_kses_allowed_html('post');
$allowed_tags['img']['srcset'] = true;
$allowed_tags['img']['sizes'] = true;

printf(
  '<div %1$s>%2$s</div>',
  $wrapper_attributes,
  wp_kses($post_items_markup, $allowed_tags)
);

複数タームの選択

ターム選択の SelectControl で複数のタームを選択できるように変更する例です。

インスペクターパネルが少しごちゃごちゃしてきたので、初期状態では以下のようにタクソノミーとタームの選択を非表示にし、Select Terms というトグルボタンを配置します。

Select Terms のトグルボタンをオンにするとタクソノミーとタームの選択を表示します。

タームの選択は複数選択できるようにプルダウンではなくリストで表示されます(離れた項目を複数選択するには command キーを押しながら選択します)。

block.json

属性 selectedTerm を selectedTerms に変更し、type を "array"、default を [] に変更します。

また、ターム選択を有効にする(表示する)かどうかの真偽値を保存する属性 enableSelectTerms を追加し、デフォルト値として false を設定します(ターム選択のコントロールは初期状態では非表示)。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl-block/my-custom-posts",
  "version": "0.1.0",
  "title": "My Custom Posts",
  "category": "widgets",
  "icon": "smiley",
  "description": "Example block scaffolded with Create Block tool.",
  "example": {},
  "supports": {
    "html": false
  },
  "attributes": {
    "myPostType": {
      "type": "string",
      "default": ""
    },
    "selectedTaxonomy": {
      "type": "string",
      "default": ""
    },
    "selectedTerms": {
      "type": "array",
      "default": []
    },
    "enableSelectTerms": {
      "type": "boolean",
      "default": false
    },
    "numberOfItems": {
      "type": "number",
      "default": 5
    },
    "order": {
      "type": "string",
      "default": "desc"
    },
    "orderBy": {
      "type": "string",
      "default": "date"
    }
  },
  "textdomain": "my-custom-posts",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

edit.js

edit.js を以下のように書き換えます。

ToggleControl を追加でインポートし、追加した属性 enableSelectTerms を attributes から分割代入で取得します。

属性 selectedTerm を selectedTerms に変更したので、selectedTerm の部分を全て selectedTerms に変更し、selectControl での値の空文字列 "" を空の配列 [] に変更ます。

投稿データを取得する getEntityRecords のパラメータでは、追加した属性 enableSelectTerms が true の場合に、タクソノミーのフィルタリングを行うようにし(40行目)、useSelect の依存配列に enableSelectTerms を追加します(60行目)。

タクソノミーとタームの selectControl の表示は、追加で enableSelectTerms が true の場合も判定条件に含めます(129行目)。

その他にも変更部分がいくつかありますが、コメントを参照ください。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import {
  PanelBody,
  QueryControls,
  SelectControl,
  ToggleControl, // ToggleControl を追加
} from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit({ attributes, setAttributes }) {
  const {
    numberOfItems,
    order,
    orderBy,
    myPostType,
    selectedTaxonomy,
    selectedTerms, // selectedTerm を selectedTerms に変更(その他の部分も)
    enableSelectTerms, // enableSelectTerms を追加
  } = attributes;

  const { posts, postTypes, taxonomies, terms } = useSelect(
    (select) => {
      const core = select("core");
      const allTaxonomies = core.getTaxonomies();
      const relatedTaxonomies = allTaxonomies
        ? allTaxonomies.filter((taxonomy) =>
            taxonomy.types.includes(myPostType),
          )
        : [];

      return {
        posts: core.getEntityRecords("postType", myPostType, {
          per_page: numberOfItems,
          _embed: "wp:featuredmedia",
          order,
          orderby: orderBy,
          // enableSelectTerms && を追加
          ...(enableSelectTerms && selectedTaxonomy && selectedTerms
            ? { [`${selectedTaxonomy}`]: selectedTerms }
            : {}),
        }),
        postTypes: core.getPostTypes({ per_page: -1 }),
        taxonomies: relatedTaxonomies,
        terms: selectedTaxonomy
          ? core.getEntityRecords("taxonomy", selectedTaxonomy, {
              per_page: -1,
            })
          : [],
      };
    },
    [
      numberOfItems,
      order,
      orderBy,
      myPostType,
      selectedTaxonomy,
      selectedTerms,
      enableSelectTerms, // enableSelectTerms を追加
    ],
  );

  const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" },
      ...(postTypes
        ? postTypes
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                ![ 'post', 'page', 'attachment' ].includes( type.slug ),
            )
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];

  const taxonomyOptions = [
    { label: __("Select a Taxonomy", "my-custom-posts"), value: "" },
    ...(taxonomies
      ? taxonomies.map((tax) => ({ label: tax.name, value: tax.slug }))
      : []),
  ];

  // ターム選択オプション
  const termOptions = [
    // デフォルトのオプションを削除
    ...(terms
      ? terms.map((term) => ({ label: term.name, value: term.id }))
      : []),
  ];

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
          <SelectControl
            label={__("Custom Post Type", "my-custom-posts")}
            value={myPostType}
            options={options}
            onChange={(value) => setAttributes({
              myPostType: value,
              selectedTaxonomy: "",
              selectedTerms: [],  // 値を "" から [] に変更
              enableSelectTerms: false,  // 追加
            })}
          />
        </PanelBody>
        <PanelBody title={__("Taxonomy Settings", "my-custom-posts")}>
          <ToggleControl
            label={__("Select Terms", "my-custom-posts")}
            checked={enableSelectTerms}
            onChange={(value) => setAttributes({ enableSelectTerms: value })}
          />
          {enableSelectTerms && myPostType && taxonomies.length > 0 && (
          <>
            <SelectControl
              label={__("Select a Taxonomy", "my-custom-posts")}
              value={selectedTaxonomy}
              options={taxonomyOptions}
              onChange={
                (value) =>
                  setAttributes({
                    selectedTaxonomy: value,
                    selectedTerms: [], // 値を "" から [] に変更
                  })
              }
            />
            {selectedTaxonomy && (
              <SelectControl
                multiple // multiple を追加
                label={__("Select Terms", "my-custom-posts")} // ラベルを Select a Term から Select Terms に変更
                value={selectedTerms}
                options={termOptions}
                onChange={(value) => setAttributes({ selectedTerms: value })}
              />
            )}
          </>
        )}
        </PanelBody>
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        {!myPostType && <p>{__("Select a post type.", "my-custom-posts")}</p>}
        {myPostType && !posts ? <p>{__("Loading...", "my-custom-posts")}</p> : null}
        <div className={`${pre}post-items`}>
          {myPostType && Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}

render.php

render.php を以下のように書き換えます。

クエリに tax_query パラメータを追加する部分の属性 selectedTerm を全て selectedTerms に変更するだけです。

<?php

$args = array(
  'posts_per_page'      => $attributes['numberOfItems'],
  'post_status'         => 'publish',
  'order'               => $attributes['order'],
  'orderby'             => $attributes['orderBy'],
  'ignore_sticky_posts' => true,
  'no_found_rows'       => true,
);

$allowed_post_types = get_post_types(['public' => true]);

if (!isset($attributes['myPostType']) || !in_array($attributes['myPostType'], $allowed_post_types, true)) {
  echo '<p>' . esc_html__('Invalid post type.', 'my-custom-posts') . '</p>';
  return;
}

$args['post_type'] = $attributes['myPostType'];

// selectedTerm を全て selectedTerms に変更
if (!empty($attributes['selectedTaxonomy']) && !empty($attributes['selectedTerms'])) {
  $args['tax_query'] = array(
    array(
      'taxonomy' => $attributes['selectedTaxonomy'],
      'field' => 'term_id',
      // $attributes['selectedTerms'] が配列と単一 ID の両方に対応済み
      'terms'    => is_array($attributes['selectedTerms']) ? array_map('intval', $attributes['selectedTerms']) : intval($attributes['selectedTerms']),
    )
  );
}

$query = new WP_Query($args);

$pre = 'wp-block-wdl-block-my-custom-posts__';

$post_items_markup = '';

$post_items_markup .= sprintf(
  '<div class="%1$spost-items">',
  esc_attr($pre)
);

if ($query->have_posts()) {
  while ($query->have_posts()) {
    $query->the_post();
    $post_link = esc_url(get_permalink());
    $title     = wp_strip_all_tags(get_the_title());

    $post_items_markup .= sprintf(
      '<div class="%1$spost-item">',
      esc_attr($pre)
    );

    if (has_post_thumbnail()) {

      $thumbnail_alt_value = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true);

      if (!$thumbnail_alt_value) {
        $thumbnail_alt_value = $title;
      }

      $featured_image = get_the_post_thumbnail(
        get_the_ID(),
        'medium',
        array(
          'alt'   => esc_attr($thumbnail_alt_value),
        )
      );

      $featured_image = sprintf(
        '<a href="%1$s" aria-label="%2$s">%3$s</a>',
        esc_url($post_link),
        esc_attr($title),
        $featured_image
      );

      $post_items_markup .= sprintf(
        '<div class="%1$sfeatured-image">%2$s</div>',
        esc_attr($pre),
        $featured_image
      );
    }

    $post_items_markup .= sprintf(
      '<%1$s class="%2$spost-title"><a href="%3$s">%4$s</a></%1$s>',
      'h3',
      esc_attr($pre),
      esc_url($post_link),
      $title
    );

    $post_items_markup .= sprintf(
      '<p class="%1$spost-excerpt">%2$s</p>',
      esc_attr($pre),
      wp_strip_all_tags(get_the_excerpt())
    );

    $post_items_markup .= "</div>\n";
  }
} else {
  $post_items_markup .= sprintf(
    '<p class="%1$sno-post">%2$s</p>',
    esc_attr($pre),
    esc_html__('No posts to display.', 'my-custom-posts')
  );
}

$post_items_markup .= "</div>\n";

wp_reset_postdata();

$wrapper_attributes = get_block_wrapper_attributes();
$allowed_tags = wp_kses_allowed_html('post');
$allowed_tags['img']['srcset'] = true;
$allowed_tags['img']['sizes'] = true;

printf(
  '<div %1$s>%2$s</div>',
  $wrapper_attributes,
  wp_kses($post_items_markup, $allowed_tags)
);

投稿や固定ページも対象にする

カスタム投稿タイプ以外に、投稿や固定ページも選択できるようにする例です。

これまでの例では投稿タイプのリストの作成で投稿と固定ページを除外していた部分を変更し、投稿を取得する際にカテゴリーやタグのパラメータを追加します。

edit.js

edit.js を以下のように書き換えます(変更が必要なのは edit.js のみです)。

投稿データを取得する際のクエリパラメータに、選択されたタクソノミー(selectedTaxonomy)がカテゴリーとタグの場合のパラメータを追加します(40,42行目)。

投稿タイプを選択する SelectControl のオプションの作成で、メディアのみを除外し、投稿と固定ページを除外しないように変更します(76行目)。

また、投稿タイプを選択するプルダウンのラベルを Custom Post Type から変更します(114行目)。

import { __ } from "@wordpress/i18n";
import { useBlockProps, InspectorControls } from "@wordpress/block-editor";
import {
  PanelBody,
  QueryControls,
  SelectControl,
  ToggleControl,
} from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import "./editor.scss";

export default function Edit({ attributes, setAttributes }) {
  const {
    numberOfItems,
    order,
    orderBy,
    myPostType,
    selectedTaxonomy,
    selectedTerms,
    enableSelectTerms,
  } = attributes;

  const { posts, postTypes, taxonomies, terms } = useSelect(
    (select) => {
      const core = select("core");
      const allTaxonomies = core.getTaxonomies();
      const relatedTaxonomies = allTaxonomies
        ? allTaxonomies.filter((taxonomy) =>
            taxonomy.types.includes(myPostType),
          )
        : [];

      return {
        posts: core.getEntityRecords("postType", myPostType, {
          per_page: numberOfItems,
          _embed: "wp:featuredmedia",
          order,
          orderby: orderBy,
          // selectedTaxonomy がカテゴリーの場合(追加)
          ...(enableSelectTerms && selectedTaxonomy && selectedTaxonomy === 'category' ? {categories: selectedTerms}: {}),
          // selectedTaxonomy がタグの場合(追加)
          ...(enableSelectTerms && selectedTaxonomy && selectedTaxonomy === 'post_tag' ? {tags: selectedTerms}: {}),
          // selectedTaxonomy がカスタムタクソノミーの場合(これまでと同じ)
          ...(enableSelectTerms && selectedTaxonomy && selectedTerms
            ? { [`${selectedTaxonomy}`]: selectedTerms }
            : {}),
        }),
        postTypes: core.getPostTypes({ per_page: -1 }),
        taxonomies: relatedTaxonomies,
        terms: selectedTaxonomy
          ? core.getEntityRecords("taxonomy", selectedTaxonomy, {
              per_page: -1,
            })
          : [],
      };
    },
    [
      numberOfItems,
      order,
      orderBy,
      myPostType,
      selectedTaxonomy,
      selectedTerms,
      enableSelectTerms,
    ],
  );

  const options = [
    { label: __("Select a Post Type", "my-custom-posts"), value: "" },
      ...(postTypes
        ? postTypes
            .filter(
              (type) =>
                type.viewable &&
                type.capabilities.create_posts !== undefined &&
                type.slug !== "attachment", // メディアのみを除外(投稿と固定ページを除外しない)
            )
            .map((type) => ({ label: type.name, value: type.slug }))
        : []),
    ];

  const taxonomyOptions = [
    { label: __("Select a Taxonomy", "my-custom-posts"), value: "" },
    ...(taxonomies
      ? taxonomies.map((tax) => ({ label: tax.name, value: tax.slug }))
      : []),
  ];

  const termOptions = [
    ...(terms
      ? terms.map((term) => ({ label: term.name, value: term.id }))
      : []),
  ];

  const pre = "wp-block-wdl-block-my-custom-posts__";

  const getTextContent = (htmlRendered) => {
    if (!htmlRendered) return "";
    const elem = document.createElement("div");
    elem.innerHTML = htmlRendered;
    return elem.textContent || elem.innerText || "";
  };

  const preventRedirect = (e) => {
    e.preventDefault();
    alert(__("Links are disabled in the editor.", "my-custom-posts"));
  };

  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Post Type Settings", "my-custom-posts")}>
          <SelectControl
            label={__("Post Type", "my-custom-posts")}   // ラベルを変更
            value={myPostType}
            options={options}
            onChange={(value) => setAttributes({
              myPostType: value,
              selectedTaxonomy: "",
              selectedTerms: [],
              enableSelectTerms: false,
            })}
          />
        </PanelBody>
        <PanelBody title={__("Taxonomy Settings", "my-custom-posts")}>
          <ToggleControl
            label={__("Select Terms", "my-custom-posts")}
            checked={enableSelectTerms}
            onChange={(value) => setAttributes({ enableSelectTerms: value })}
          />
          {enableSelectTerms && myPostType && taxonomies.length > 0 && (
          <>
            <SelectControl
              label={__("Select a Taxonomy", "my-custom-posts")}
              value={selectedTaxonomy}
              options={taxonomyOptions}
              onChange={
                (value) =>
                  setAttributes({
                    selectedTaxonomy: value,
                    selectedTerms: [],
                  })
              }
            />
            {selectedTaxonomy && (
              <SelectControl
                multiple
                label={__("Select Terms", "my-custom-posts")}
                value={selectedTerms}
                options={termOptions}
                onChange={(value) => setAttributes({ selectedTerms: value })}
              />
            )}
          </>
        )}
        </PanelBody>
        <PanelBody title={__("Content Settings", "my-custom-posts")}>
          <QueryControls
            numberOfItems={numberOfItems}
            onNumberOfItemsChange={(value) =>
              setAttributes({ numberOfItems: value })
            }
            minItems={1}
            maxItems={10}
            order={order}
            orderBy={orderBy}
            onOrderChange={(value) => setAttributes({ order: value })}
            onOrderByChange={(value) => setAttributes({ orderBy: value })}
          />
        </PanelBody>
      </InspectorControls>
      <div {...useBlockProps()}>
        {!myPostType && <p>{__("Select a post type.", "my-custom-posts")}</p>}
        {myPostType && !posts ? <p>{__("Loading...", "my-custom-posts")}</p> : null}
        <div className={`${pre}post-items`}>
          {myPostType && Array.isArray(posts) && posts.length === 0 && (
            <p className={`${pre}no-post`}>{__("No posts to display.", "my-custom-posts")}</p>
          )}
          {posts &&
            posts.map((post) => {
              const featuredMediaUrl =
                post._embedded?.["wp:featuredmedia"]?.[0]?.media_details?.sizes?.medium?.source_url;
              const altText =
                post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text || getTextContent(post.title?.rendered);

              return (
                <div key={post.id} className={`${pre}post-item`}>
                  {featuredMediaUrl && (
                    <div className={`${pre}featured-image`}>
                      <a href={post.link} onClick={preventRedirect}>
                        <img src={featuredMediaUrl} alt={altText} />
                      </a>
                    </div>
                  )}
                  {post.title?.rendered && (
                    <h3 className={`${pre}post-title`}>
                      <a href={post.link} onClick={preventRedirect}>
                        {getTextContent(post.title.rendered)}
                      </a>
                    </h3>
                  )}
                  {post.excerpt?.rendered && (
                    <p className={`${pre}post-excerpt`}>
                      {getTextContent(post.excerpt.rendered)}
                    </p>
                  )}
                </div>
              );
            })}
        </div>
      </div>
    </>
  );
}

例えば、投稿を選択するとタクソノミーでカテゴリーとタグが選択できます。

デフォルトを投稿(post)にする

現在はブロックを挿入した時点では投稿タイプが選択されていませんが、block.json で属性 myPostType にデフォルト値として "post" を設定すれば、初期状態で投稿が表示されます。

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "wdl-block/my-custom-posts",
  "version": "0.1.0",
  "title": "My Custom Posts",
  "category": "widgets",
  "icon": "smiley",
  "description": "Example block scaffolded with Create Block tool.",
  "example": {},
  "supports": {
    "html": false
  },
  "attributes": {
    "myPostType": {
      "type": "string",
      "default": "post"
    },
    "selectedTaxonomy": {
      "type": "string",
      "default": ""
    },
    "selectedTerms": {
      "type": "array",
      "default": []
    },
    "enableSelectTerms": {
      "type": "boolean",
      "default": false
    },
    "numberOfItems": {
      "type": "number",
      "default": 5
    },
    "order": {
      "type": "string",
      "default": "desc"
    },
    "orderBy": {
      "type": "string",
      "default": "date"
    }
  },
  "textdomain": "my-custom-posts",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScript": "file:./view.js"
}

edit.js や render.php はそのままで機能しますが、render.php の投稿タイプの検証部分を以下のように、投稿タイプの値が不正だった場合のデフォルトを post にすることもできます。

<?php

$args = array(
  'posts_per_page'      => $attributes['numberOfItems'],
  'post_status'         => 'publish',
  'order'               => $attributes['order'],
  'orderby'             => $attributes['orderBy'],
  'ignore_sticky_posts' => true,
  'no_found_rows'       => true,
);

$custom_post_type = $attributes['myPostType'];

$allowed_post_types = get_post_types(['public' => true]);

// 投稿タイプの値が不正(存在しない投稿タイプや空文字)だった場合のデフォルトを 'post' に
if (!in_array($custom_post_type, $allowed_post_types, true)) {
  $custom_post_type = 'post';
}

$args['post_type'] = $custom_post_type;

if (!empty($attributes['selectedTaxonomy']) && !empty($attributes['selectedTerms'])) {
  $args['tax_query'] = array(
    array(
      'taxonomy' => $attributes['selectedTaxonomy'],
      'field' => 'term_id',
      'terms'    => is_array($attributes['selectedTerms']) ? array_map('intval', $attributes['selectedTerms']) : intval($attributes['selectedTerms']),
    )
  );
}

$query = new WP_Query($args);

$pre = 'wp-block-wdl-block-my-custom-posts__';

$post_items_markup = '';

$post_items_markup .= sprintf(
  '<div class="%1$spost-items">',
  esc_attr($pre)
);

if ($query->have_posts()) {
  while ($query->have_posts()) {
    $query->the_post();
    $post_link = esc_url(get_permalink());
    $title     = wp_strip_all_tags(get_the_title());

    $post_items_markup .= sprintf(
      '<div class="%1$spost-item">',
      esc_attr($pre)
    );

    if (has_post_thumbnail()) {

      $thumbnail_alt_value = get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true);

      if (!$thumbnail_alt_value) {
        $thumbnail_alt_value = $title;
      }

      $featured_image = get_the_post_thumbnail(
        get_the_ID(),
        'medium',
        array(
          'alt'   => esc_attr($thumbnail_alt_value),
        )
      );

      $featured_image = sprintf(
        '<a href="%1$s" aria-label="%2$s">%3$s</a>',
        esc_url($post_link),
        esc_attr($title),
        $featured_image
      );

      $post_items_markup .= sprintf(
        '<div class="%1$sfeatured-image">%2$s</div>',
        esc_attr($pre),
        $featured_image
      );
    }

    $post_items_markup .= sprintf(
      '<%1$s class="%2$spost-title"><a href="%3$s">%4$s</a></%1$s>',
      'h3',
      esc_attr($pre),
      esc_url($post_link),
      $title
    );

    $post_items_markup .= sprintf(
      '<p class="%1$spost-excerpt">%2$s</p>',
      esc_attr($pre),
      wp_strip_all_tags(get_the_excerpt())
    );

    $post_items_markup .= "</div>\n";
  }
} else {
  $post_items_markup .= sprintf(
    '<p class="%1$sno-post">%2$s</p>',
    esc_attr($pre),
    esc_html__('No posts to display.', 'my-custom-posts')
  );
}

$post_items_markup .= "</div>\n";

wp_reset_postdata();

$wrapper_attributes = get_block_wrapper_attributes();
$allowed_tags = wp_kses_allowed_html('post');
$allowed_tags['img']['srcset'] = true;
$allowed_tags['img']['sizes'] = true;

printf(
  '<div %1$s>%2$s</div>',
  $wrapper_attributes,
  wp_kses($post_items_markup, $allowed_tags)
);