独自の「ABテスト」ブロックを追加する方法

Cocoonカスタマイズ集
この記事は約17分で読めます。
記事内に広告が含まれています。

はじめに

Xwriteなどには「ABテスト」ブロックがあり、パターンA、Bをランダム表示し、どちらが閲覧者にとって効果があるのかクリック数を集計することができます。

そこで、パターンに登録したアフェリエイトなどのリンクタグをランダムで表示する「ABテスト」ブロックを作成します。

この記事では、この方法について説明します。

完成イメージ

実装手順

以下のコードをfunctions.phpに追加します。

// ブロックを登録
add_action('init', function () {
  $handle = 'ab-test-block';

  wp_register_script($handle, '', ['wp-blocks', 'wp-element', 'wp-i18n', 'wp-components', 'wp-block-editor'], false, true);

  register_block_type('custom/ab-test-block', [
    'render_callback' => 'render_ab_test_block',
    'editor_script'   => $handle,
  ]);
});


// エディター用JS・CSSを追加
add_action('enqueue_block_editor_assets', function () {
  $handle = 'ab-test-block';

  // 再利用ブロック(wp_block)の一覧を取得
  $blocks = get_posts([
    'post_type'      => 'wp_block',
    'posts_per_page' => -1,
  ]);

  $options = [];
  $stats   = [];
  $post_id = get_the_ID();

  // 各ブロックの選択肢と統計データを取得
  foreach ($blocks as $b) {
    $options[]     = ['label' => $b->post_title, 'value' => strval($b->ID)];
    $stats[$b->ID] = json_decode(get_post_meta($post_id, 'my_abtest_stats_' . $b->ID, true), true) ?? ['views' => 0, 'clicks' => 0];
  }

  // JSから参照するデータを設定
  wp_localize_script($handle, 'ABTestBlocks', [
    'blocks' => $options,
    'stats'  => $stats,
  ]);

  ob_start();
  ?>
  (function (wp) {
    const { registerBlockType } = wp.blocks;
    const { InspectorControls } = wp.blockEditor;
    const { PanelBody, SelectControl, RangeControl } = wp.components;

    registerBlockType('custom/ab-test-block', {
      title: 'ABテスト',
      icon: 'randomize',
      category: 'cocoon-block',
      attributes: {
        blockA: { type: 'string', default: '' },
        blockB: { type: 'string', default: '' },
        ratioA: { type: 'number', default: 50 }
      },

      /**
       * エディター描画コンポーネント
       *
       * @param {Object} props ブロックのProps
       * @return {import('react').ReactElement} Reactエレメント
       */
      edit: function (props) {
        const { attributes, setAttributes } = props;

        // ドロップダウン選択肢を生成
        const options = [{ label: '未選択', value: '' }].concat(
          ABTestBlocks.blocks.length ? ABTestBlocks.blocks : []
        );

        // 各パターンの表示回数・クリック数・クリック率を取得
        const statsA = ABTestBlocks.stats[attributes.blockA] || { views: 0, clicks: 0 };
        const statsB = ABTestBlocks.stats[attributes.blockB] || { views: 0, clicks: 0 };
        const rateA  = statsA.views ? Math.round((statsA.clicks / statsA.views) * 100) : 0;
        const rateB  = statsB.views ? Math.round((statsB.clicks / statsB.views) * 100) : 0;

        // エディターUIを表示
        return wp.element.createElement('div', {
          style: { border: '1px dashed #ccc', padding: '10px', marginBottom: '10px' }
        },
          // サイドバー設定を表示
          wp.element.createElement(InspectorControls, {},
            wp.element.createElement(PanelBody, { title: 'ABテスト設定' },
              wp.element.createElement(SelectControl, {
                label: 'パターンA',
                value: attributes.blockA || '',
                options: options,
                onChange: function (val) {
                  setAttributes({ blockA: val });
                }
              }),
              wp.element.createElement(SelectControl, {
                label: 'パターンB',
                value: attributes.blockB || '',
                options: options,
                onChange: function (val) {
                  setAttributes({ blockB: val });
                }
              }),
              wp.element.createElement(RangeControl, {
                label: 'Aの表示率 (%)',
                value: attributes.ratioA,
                onChange: function (val) {
                  // 10%単位に丸め込み、10~90%の範囲に調整
                  let rounded = Math.round(val / 10) * 10;

                  if (rounded < 10) {
                    rounded = 10;
                  }

                  if (rounded > 90) {
                    rounded = 90;
                  }

                  setAttributes({ ratioA: rounded });
                },
                min: 10,
                max: 90,
                step: 1
              })
            )
          ),

          // エディター内プレビューを表示
          wp.element.createElement('div', { style: { display: 'flex', gap: '10px' } },

            // パターンA
            wp.element.createElement('div', { style: { flex: '1', border: '1px solid #ccc', padding: '5px' } },
              wp.element.createElement('b', {}, 'パターンA(' + attributes.ratioA + '%)'),
              wp.element.createElement('br', {}),
              options.find(function (o) {
                return o.value === attributes.blockA;
              })?.label || '未選択',
              wp.element.createElement('br', {}),
              '表示回数:' + statsA.views,
              wp.element.createElement('br', {}),
              'クリック数:' + statsA.clicks,
              wp.element.createElement('br', {}),
              'クリック率:' + rateA + '%'
            ),

            // パターンB
            wp.element.createElement('div', { style: { flex: '1', border: '1px solid #ccc', padding: '5px' } },
              wp.element.createElement('b', {}, 'パターンB(' + (100 - attributes.ratioA) + '%)'),
              wp.element.createElement('br', {}),
              options.find(function (o) {
                return o.value === attributes.blockB;
              })?.label || '未選択',
              wp.element.createElement('br', {}),
              '表示回数:' + statsB.views,
              wp.element.createElement('br', {}),
              'クリック数:' + statsB.clicks,
              wp.element.createElement('br', {}),
              'クリック率:' + rateB + '%'
            )
          )
        );
      },

      /**
       * 保存用コンポーネント
       *
       * @return {import('react').ReactElement} Reactエレメント
       */
      save: function () {
        return null;
      }
    });
  })(window.wp);
  <?php
  wp_add_inline_script($handle, ob_get_clean());
});


/**
 * ABテストブロックを描画
 *
 * @param array $attributes ブロックの属性
 * @return string 生成されたHTML
 */
if (!function_exists('render_ab_test_block')):
function render_ab_test_block($attributes) {
  // パターン未選択時の処理
  if (empty($attributes['blockA']) || empty($attributes['blockB'])) {
    return '<p>ABテスト用のブロックが選択されていません。</p>';
  }

  // 設定された確率に基づいてA/Bを判定
  $ratioA  = intval($attributes['ratioA'] ?? 50);
  $variant = (rand(1, 100) <= $ratioA) ? 'A' : 'B';

  // 選択されたブロックを取得
  $blockA_id         = intval($attributes['blockA']);
  $blockB_id         = intval($attributes['blockB']);
  $selected_block_id = ($variant === 'A') ? $blockA_id : $blockB_id;
  $selected_post     = get_post($selected_block_id);

  // 対象ブロックが存在しない場合の処理
  if (!$selected_post) {
    return '<p>ABテスト対象ブロックが見つかりません。</p>';
  }

  // 選択された再利用ブロックを展開
  $blocks = parse_blocks($selected_post->post_content);
  $html   = '';

  foreach ($blocks as $block) {
    if ($block['blockName'] === 'custom/ab-test-block') {
      continue;
    }

    $html .= render_block($block);
  }

  // CSSクラスを設定
  $classes = 'ab-test ab-' . $variant;

  return '<div class="' . esc_attr($classes) . '" data-abtest-block="' . $selected_block_id . '" data-abtest-variant="' . esc_attr($variant) . '" data-abtest-clickable="1">' . $html . '</div>';
}
endif;


/**
 * ABテストの計測処理
 *
 * @param string $type 計測種別(views または clicks)
 * @return void
 */
if (!function_exists('abtest_track_event')):
function abtest_track_event($type) {
  // ログイン中ユーザーを計測対象から除外
  if (is_user_logged_in()) {
    wp_send_json_success();
  }

  // リクエストパラメータを取得
  $post_id  = intval($_POST['post_id'] ?? 0);
  $block_id = intval($_POST['block_id'] ?? 0);

  if (!$post_id || !$block_id) {
    wp_send_json_error();
  }

  // 現在の統計データを取得
  $meta_key = 'my_abtest_stats_' . $block_id;
  $stats    = get_post_meta($post_id, $meta_key, true);
  $stats    = $stats ? json_decode($stats, true) : ['views' => 0, 'clicks' => 0];

  // 計測値を更新
  if (isset($stats[$type])) {
    $stats[$type]++;
    update_post_meta($post_id, $meta_key, json_encode($stats));
  }

  wp_send_json_success();
}
endif;


// Ajaxアクションフックを登録
add_action('wp_ajax_nopriv_abtest_view', function () {
  abtest_track_event('views');
});

add_action('wp_ajax_nopriv_abtest_click', function () {
  abtest_track_event('clicks');
});


// フロント用JS・CSSを追加
add_action('wp_enqueue_scripts', function () {
  if (!is_single() || is_user_logged_in()) {
    return;
  }

  $post_id = get_queried_object_id();

  // JSから参照するデータを設定
  $data = [
    'ajaxUrl' => admin_url('admin-ajax.php'),
    'postId'  => $post_id,
  ];

  ob_start();
  ?>
  (function () {
    const config = <?php echo json_encode($data); ?>;

    function attachABBlocks() {
      document.querySelectorAll('.ab-test[data-abtest-clickable]').forEach(function (div) {
        if (div.dataset.abtestAttached) {
          return;
        }

        const blockId = div.dataset.abtestBlock;

        // 表示回数を送信
        navigator.sendBeacon(
          config.ajaxUrl,
          new URLSearchParams({
            action: 'abtest_view',
            post_id: config.postId,
            block_id: blockId
          })
        );

        // クリック数を1回だけ送信
        div.addEventListener('click', function () {
          if (!div.dataset.abtestClicked) {
            fetch(config.ajaxUrl, {
              method: 'POST',
              body: new URLSearchParams({
                action: 'abtest_click',
                post_id: config.postId,
                block_id: blockId
              })
            });

            div.dataset.abtestClicked = '1';
          }
        });

        div.dataset.abtestAttached = '1';
      });
    }

    attachABBlocks();

    const observer = new MutationObserver(attachABBlocks);
    observer.observe(document.body, { childList: true, subtree: true });
  })();
  <?php

  $handle = 'ab-test-block';

  wp_register_script($handle, false, [], false, true);
  wp_enqueue_script($handle);
  wp_add_inline_script($handle, ob_get_clean());
});

さいごに

今回紹介した方法を使えば、Gutenbergで独自のABテストブロックを追加し、パターンA/Bのクリック数や表示率を計測することができます。これにより、効果的なリンクやコンテンツを簡単に分析可能です。

タイトルとURLをコピーしました