【@wordpress/scripts】でオリジナルブロック作成(リッチテキストツールバー、動的CTAリンク)

@wordpress/scriptsとは

@wordpress/scriptsは、WordPressのブロックエディタやプラグイン開発のためのビルドツールパッケージです。

React・JSXを使ったモダンなJavaScript開発環境をWordPressプロジェクトに簡単に導入できます!

webpackやBabelなどの複雑な設定を内包しており、開発者は設定ファイルを書かずにすぐに開発を始められます。ビルドコマンドや開発用の監視モード、ESLintやJestなどのテスト環境も統合されています

リッチテキストツールバーブロックの作成手順

パッケージインストール

npm プロジェクトの初期化

package.json ファイルを作成します。

npm init -y

@wordpress/scriptsのインストール

(WordPress のブロックエディター用のパッケージ)

npm install @wordpress/scripts @wordpress/blocks @wordpress/i18n @wordpress/block-editor @wordpress/components @wordpress/data react react-dom

package.jsonにスクリプトが追記

{
  "name": "my-theme-custom-block",
  "version": "1.0.0",
  "description": "My first WordPress custom block in a theme",
  "main": "build/index.js",
  "scripts": {
    <strong>"build": "wp-scripts build",
    "start": "wp-scripts start"</strong>
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@wordpress/scripts": "^x.x.x"
  }
}

@wordpress パッケージの型定義ファイルインストール

npm install --save-dev @types/wordpress__blocks
npm install --save-dev @types/wordpress__block-editor @types/wordpress__components
npm install @wordpress/data @wordpress/rich-text
npm install --save-dev @types/react @types/wordpress__rich-text

webpack

  • ES6+やTypeScriptのコードを古いブラウザでも動作するES5に変換できます。
  • モジュール間の依存関係を自動的に解析し、複数のJavaScriptファイルを1つのファイルにまとめます。
webpackでのTypeScript処理フロー TypeScript .ts ファイル ts-loader TypeScript → JS Babel ES6+ → ES5 出力 bundle.js

webpack.config.jsの設定

webpack.config.jsは、webpackの設定ファイルです。

webpack.config.js の主要構造 entry アプリケーションの 開始点を指定 output バンドルファイルの 出力先と名前を指定 module.rules ファイルの変換方法を ローダーで指定 plugins 追加の処理や最適化を プラグインで指定 resolve モジュールの解決方法を カスタマイズ mode development, production などの環境を指定

webpack.config.js

const defaultConfig = require('@wordpress/scripts/config/webpack.config');
const path = require('path');

module.exports = {
    ...defaultConfig,

    // エントリーポイントの設定
    entry: {
        lead: path.resolve(__dirname, 'wp-content/themes/originaltheme/src/lead/index.tsx'),
    },

    // 出力の設定
    output: {
        path: path.resolve(__dirname, 'wp-content/themes/originaltheme/build'),
        filename: '[name].js', // [name] には entry で指定したキーが入る
    },

    // ファイルの拡張子を省略できるようにする
    resolve: {
        extensions: ['.ts', '.tsx', '.js']
    },


    module: {

        // ローダーの設定
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    }
};
npm install ts-loader --save-dev

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",                          // 出力するJavaScriptのバージョン
    "module": "commonjs",                     // モジュールシステム
    "strict": true,                           // 厳格な型チェックオプション
    "esModuleInterop": true,                  // ESモジュールとの互換性
    "skipLibCheck": true,                     // ライブラリの型チェックをスキップ
    "forceConsistentCasingInFileNames": true, // ファイル名の大文字小文字の一貫性を強制
    "jsx": "react",                           // JSXのサポート
    "moduleResolution": "node",               // モジュール解決方法
    "resolveJsonModule": true,                // JSONモジュールのインポートを許可
    "outDir": "./build",                      // 出力ディレクトリ
    "rootDir": "./wp-content/themes/originaltheme/src" // ソースファイルのルートディレクトリ
  },
  "include": [
    "wp-content/themes/originaltheme/src/**/*" // コンパイル対象のファイル
  ],
  "exclude": [
    "node_modules"                            // コンパイル対象外のファイル
  ]
}

ブロックのソースファイルを作成

リッチテキストツールバーを使用

インライン単位で文字サイズ文字色を変更できるブロック

index.tsx

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
    useBlockProps,
    RichText,
    RichTextToolbarButton,
} from '@wordpress/block-editor';
import {
    registerFormatType,
    applyFormat,
    removeFormat,
    type RichTextValue
} from '@wordpress/rich-text';
import React from 'react';

// ブロックの属性の型定義
interface BlockAttributes {
    content: string;
}

// フォーマットの型定義
interface FormatProps {
    isActive: boolean;
    value: RichTextValue;
    onChange: (value: RichTextValue) => void;
}

// WPFormat の型定義
interface WPFormatType {
    name: string;
    title: string;
    tagName: string;
    className: string;
    interactive: boolean;
    edit: (props: FormatProps) => JSX.Element;
}

// フォーマット名の定義
const FONT_SIZE_FORMAT = 'custom-format/font-size';
const TEXT_COLOR_FORMAT = 'custom-format/text-color';

// フォントサイズフォーマットの登録
const fontSizeFormat: WPFormatType = {
    name: FONT_SIZE_FORMAT,
    title: __('Font Size', 'custom-format'),
    tagName: 'span',
    className: 'custom-font-size',
    interactive: false,
    edit: ({ isActive, value, onChange }: FormatProps) => {
        const fontSizes = ['0.75rem', '1rem', '2rem', '3rem', '4rem'];

        // フォントサイズの変更の処理の関数
        const onChangeFontSize = (size: string) => {
            const newFormat = {
                type: FONT_SIZE_FORMAT,
                attributes: {
                    style: `font-size: ${size};`,
                },
            };

            if (isActive) {
                onChange(removeFormat(value, FONT_SIZE_FORMAT));
            }
            onChange(applyFormat(value, newFormat));
        };

        return (
            <React.Fragment>
                {fontSizes.map((size) => (
                    <RichTextToolbarButton
                        key={size}
                        icon="editor-textcolor"
                        title={`${size} ${__('Font Size', 'custom-format')}`}
                        onClick={() => onChangeFontSize(size)}
                        isActive={isActive}
                    />
                ))}
            </React.Fragment>
        );
    }
};

// 文字色フォーマットの登録
const textColorFormat: WPFormatType = {
    name: TEXT_COLOR_FORMAT,
    title: __('Red Text', 'custom-format'),
    tagName: 'span',
    className: 'custom-text-color',
    interactive: false,
    edit: ({ isActive, value, onChange }: FormatProps) => {

        // 文字色の変更の処理の関数
        const onToggleColor = () => {
            if (isActive) {
                onChange(removeFormat(value, TEXT_COLOR_FORMAT));
            } else {
                const newFormat = {
                    type: TEXT_COLOR_FORMAT,
                    attributes: {
                        style: 'color: red;',
                    },
                };
                onChange(applyFormat(value, newFormat));
            }
        };

        return (
            <RichTextToolbarButton
                icon="editor-textcolor"
                title={__('Red Text', 'custom-format')}
                onClick={onToggleColor}
                isActive={isActive}
            />
        );
    }
};

// フォーマットの登録
registerFormatType(FONT_SIZE_FORMAT, fontSizeFormat);
registerFormatType(TEXT_COLOR_FORMAT, textColorFormat);

// ブロックを登録
registerBlockType<BlockAttributes>('customtheme/cardlead', {
    title: __('カード記事リード文', 'custom-block'),
    icon: 'editor-textcolor',
    category: 'text',

    attributes: {
        content: {
            type: 'string',
            source: 'html',
            selector: 'p',
        },
    },

    edit: ({ attributes, setAttributes }) => {
        const { content } = attributes;
        const blockProps = useBlockProps({
            className: 'my-custom-class',
            style: {
                borderTop: '8px solid #ffda00',
                borderBottom: '8px solid #ffda00',
                textAlign: 'center' as 'center',
            }
        });

        return (
            <div {...blockProps}>
                <RichText
                    tagName="p"
                    value={content}
                    onChange={(newContent: string) => setAttributes({ content: newContent })}
                    placeholder={__('Select text and choose font size or color...', 'custom-block')}
                    allowedFormats={[
                        'core/bold',
                        'core/italic',
                        FONT_SIZE_FORMAT,
                        TEXT_COLOR_FORMAT
                    ]}
                />
            </div>
        );
    },

    save: ({ attributes }) => {
        const { content } = attributes;
        const blockProps = useBlockProps.save({
            className: 'my-custom-class',
            style: {
                borderTop: '8px solid #ffda00',
                borderBottom: '8px solid #ffda00',
                textAlign: 'center' as 'center',
            }
        });

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

functions.phpで読み込み

functions.php

<?php
// カスタムブロックの登録
function my_theme_custom_block_init() {
    // register_block_type()の第一引数は「ドメイン名/ブロック名」でregisterBlockType()の第一引数と一致させる
    register_block_type( 'customtheme/cardlead', array(
        'editor_script' => 'cardlead-script',
    ) );
}
add_action( 'init', 'my_theme_custom_block_init' );

// ブロックのスクリプトを読み込む
function my_theme_custom_block_enqueue_assets() {
    $asset_file_cardlead = include( get_template_directory() . '/build/cardlead.asset.php' );

    wp_enqueue_script(
        'cardlead-script',
        get_template_directory_uri() . '/build/cardlead.js',
        $asset_file_cardlead['dependencies'],
        $asset_file_cardlead['version']
    );
}
add_action( 'enqueue_block_editor_assets', 'my_theme_custom_block_enqueue_assets' );

動的CTAブロック作成手順

動的CTAブロックの内容は、管理画面上でテキスト、ボタン

【CTAに付与されるパラメータ例】
dycta=t101_t203_b101
※上記をCV時URLに付与する

文言1文言1 パラメータ文言1 排出率文言2文言2 パラメータ文言2 排出率CTAボタンCTAボタン パラメータCTAボタン 排出率
初回申し込みは無料t10130%24時間以内に20人が予約しましたt20120%今すぐメールでお問合せ30%b101
無料トライアルt10230%期間限定となってますt20210%お気軽にお問い合わせください40%b102
たったの3ステップ申し込みフォームt10340%今なら限定特典もt20370%お申込みはこちら30%b103

block.json

block.jsonは、WordPressのブロックエディタで使用するカスタムブロックのメタデータを定義するJSONファイルです。

ブロックの名前、説明、カテゴリ、アイコン、対応する属性、使用するJavaScriptやCSSファイルのパスなどを記述します。このファイルを使うことで、

ブロックの設定を一元管理でき、PHPとJavaScriptの両方からブロック情報にアクセスできるようになります。WordPress 5.8以降で推奨される方式で、従来のPHPでの登録方法よりもシンプルで保守性が高いのが特徴です。

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "theme-custom/cta",
    "title": "動的CTA",
    "category": "widgets",
    "icon": "megaphone",
    "description": "ABテスト可能な動的CTAブロック",
    "supports": {
        "html": false
    },
    "attributes": {
        "ctaUrl": {
            "type": "string",
            "default": "https://10-10-10.jp/mailform/"
        },
        "text01": {
            "type": "array",
            "default": [
                { "text": "初回申し込みは無料", "param": "t101", "rate": 30 },
                { "text": "無料トライアル", "param": "t102", "rate": 30 },
                { "text": "たったの3ステップ申し込みフォーム", "param": "t103", "rate": 40 }
            ]
        },
        "text02": {
            "type": "array",
            "default": [
                { "text": "24時間以内に20人が予約しました", "param": "t201", "rate": 20 },
                { "text": "期間限定となってます", "param": "t202", "rate": 10 },
                { "text": "今なら限定特典も", "param": "t203", "rate": 70 }
            ]
        },
        "ctaButton": {
            "type": "array",
            "default": [
                { "text": "今すぐメールでお問合せ", "param": "b101", "rate": 30 },
                { "text": "お気軽にお問い合わせください", "param": "b102", "rate": 40 },
                { "text": "お申込みはこちら", "param": "b103", "rate": 30 }
            ]
        }
    },
    "textdomain": "theme-custom",
    "editorScript": "file:./dynamicCta.js"
}

supports html falseは、このブロックのHTMLコードを直接編集する機能を無効にする設定です。trueにするとエディタでHTMLとして編集できますが、動的ブロックでは通常falseにしてビジュアル編集のみに制限します。

attributesは、編集画面でユーザーが変更できるブロックの内容を保存するためのデータ定義です。

editorScriptは、ブロックエディタで読み込むJavaScriptファイルを指定

index.tsx

import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';

// CTAテキストの型定義
export interface CtaTextOption {
  text: string;
  param: string;
  rate: number;
}

// ブロックの属性型定義
export type DynamicCtaAttributes = {
  ctaUrl: string;
  text01: CtaTextOption[];
  text02: CtaTextOption[];
  ctaButton: CtaTextOption[];
}

registerBlockType<DynamicCtaAttributes>('theme-custom/cta', {
  edit: Edit,
  save: () => null,
});

edit.tsx

import { useBlockProps } from '@wordpress/block-editor';
import { TextControl, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { DynamicCtaAttributes, CtaTextOption } from './index';

const Edit = ({ attributes, setAttributes }: {
  attributes: DynamicCtaAttributes;
  setAttributes: (attrs: Partial<DynamicCtaAttributes>) => void;
}) => {
  const blockProps = useBlockProps();

  const addOption = (source: 'text01' | 'text02' | 'ctaButton') => {
    setAttributes({
      [source]: [
        ...attributes[source],
        { text: '', param: '', rate: 0 }
      ]
    });
  };

  const updateOption = (
    source: 'text01' | 'text02' | 'ctaButton',
    index: number,
    field: keyof CtaTextOption,
    value: string | number
  ) => {
    const newOptions = [...attributes[source]];
    newOptions[index] = { ...newOptions[index], [field]: value };
    setAttributes({ [source]: newOptions });
  };

  const removeOption = (source: 'text01' | 'text02' | 'ctaButton', index: number) => {
    setAttributes({
      [source]: attributes[source].filter((_, i) => i !== index)
    });
  };

  const renderTable = (source: 'text01' | 'text02' | 'ctaButton', title: string) => {
    return (
      <div style={{ marginTop: '1.5rem' }}>
        <h4>{title}</h4>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ backgroundColor: '#f0f0f0' }}>
              <th style={{ border: '1px solid #ddd', padding: '8px', width: '120px' }}>パラメータ</th>
              <th style={{ border: '1px solid #ddd', padding: '8px' }}>テキスト</th>
              <th style={{ border: '1px solid #ddd', padding: '8px', width: '100px' }}>出現率(%)</th>
              <th style={{ border: '1px solid #ddd', padding: '8px', width: '80px' }}>削除</th>
            </tr>
          </thead>
          <tbody>
            {attributes[source].map((option, index) => (
              <tr key={index}>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  <input
                    type="text"
                    value={option.param}
                    onChange={(e) => updateOption(source, index, 'param', e.target.value)}
                    style={{ width: '100%' }}
                    placeholder="例: t101"
                  />
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  <input
                    type="text"
                    value={option.text}
                    onChange={(e) => updateOption(source, index, 'text', e.target.value)}
                    style={{ width: '100%' }}
                    placeholder="テキストを入力"
                  />
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                  <input
                    type="number"
                    value={option.rate}
                    onChange={(e) => updateOption(source, index, 'rate', Number(e.target.value))}
                    style={{ width: '100%' }}
                    min="0"
                    max="100"
                  />
                </td>
                <td style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'center' }}>
                  <Button isDestructive onClick={() => removeOption(source, index)}>
                    削除
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
        <Button isPrimary onClick={() => addOption(source)} style={{ marginTop: '0.5rem' }}>
          追加
        </Button>
      </div>
    );
  };

  return (
    <div {...blockProps} style={{ border: '1px solid #ccc', padding: '1rem' }}>
      <h3>{__('動的CTA設定', 'theme-custom')}</h3>
      
      <TextControl
        label={__('CTA URL', 'theme-custom')}
        value={attributes.ctaUrl}
        onChange={(value) => setAttributes({ ctaUrl: value })}
      />

      {renderTable('text01', '文言1 (text01)')}
      {renderTable('text02', '文言2 (text02)')}
      {renderTable('ctaButton', 'CTAボタン (ctaButton)')}
    </div>
  );
};

export default Edit;

functions.php

// 動的CTAブロックの登録
function register_dynamic_cta_block()
{
    register_block_type(get_template_directory() . '/blocks/dynamic-cta', array(
        'render_callback' => 'render_dynamic_cta_block'
    ));
}
add_action('init', 'register_dynamic_cta_block');

// 動的CTAブロックのレンダリング
function render_dynamic_cta_block($attributes)
{
    $ctaUrl = isset($attributes['ctaUrl']) ? $attributes['ctaUrl'] : 'https://10-10-10.jp/mailform/';
    $text01 = isset($attributes['text01']) ? $attributes['text01'] : [];
    $text02 = isset($attributes['text02']) ? $attributes['text02'] : [];
    $ctaButton = isset($attributes['ctaButton']) ? $attributes['ctaButton'] : [];

    // ランダムにテキストを選択(出現率に基づく)
    $selectedText01 = select_random_by_rate($text01);
    $selectedText02 = select_random_by_rate($text02);
    $selectedButton = select_random_by_rate($ctaButton);

    // パラメータを結合
    $params = implode('_', array_filter([
        $selectedText01['param'],
        $selectedText02['param'],
        $selectedButton['param']
    ]));
    
    $finalUrl = $ctaUrl . '?dycta=' . $params;

    ob_start();
    ?>
<!-- デバッグ情報 -->
<!-- 
========== text01 ==========
<?php foreach ($text01 as $i => $item): ?>
[<?php echo $i; ?>] <?php echo $item['text']; ?> (param: <?php echo $item['param']; ?>, rate: <?php echo $item['rate']; ?>)
<?php endforeach; ?>

========== text02 ==========
<?php foreach ($text02 as $i => $item): ?>
[<?php echo $i; ?>] <?php echo $item['text']; ?> (param: <?php echo $item['param']; ?>, rate: <?php echo $item['rate']; ?>)
<?php endforeach; ?>

========== ctaButton ==========
<?php foreach ($ctaButton as $i => $item): ?>
[<?php echo $i; ?>] <?php echo $item['text']; ?> (param: <?php echo $item['param']; ?>, rate: <?php echo $item['rate']; ?>)
<?php endforeach; ?>

========== 選択された結果 ==========
text01: <?php echo $selectedText01['text']; ?> (<?php echo $selectedText01['param']; ?>)
text02: <?php echo $selectedText02['text']; ?> (<?php echo $selectedText02['param']; ?>)
button: <?php echo $selectedButton['text']; ?> (<?php echo $selectedButton['param']; ?>)
パラメータ: <?php echo $params; ?>
-->


    <div class="dynamic-cta">
        <div class="cta-text-01"><?php echo esc_html($selectedText01['text']); ?></div>
        <div class="cta-text-02"><?php echo esc_html($selectedText02['text']); ?></div>
        <a href="<?php echo esc_url($finalUrl); ?>" class="cta-button">
            <?php echo esc_html($selectedButton['text']); ?>
        </a>
    </div>
    <?php
    return ob_get_clean();
}

function select_random_by_rate($options) {
    if (empty($options)) return ['text' => '', 'param' => '', 'rate' => 0];
    
    $total = array_sum(array_column($options, 'rate')); // 確率の合計を計算
    $rand = rand(1, $total); // 
    $sum = 0;
    
    foreach ($options as $option) {
        $sum += $option['rate'];
        if ($rand <= $sum) {
            return $option;
        }
    }
    
    return $options[0];
}