クリックひとつで運勢を占い、引いた回数まで自動でカウント――そんな“おみくじ箱”を、WordPress とシンプルな JavaScript だけで実装してみました。REST API を使って累計をデータベースに保存し、紙吹雪アニメで当たり外れを演出。プラグインを増やさずサイトを軽量に保ちつつ、訪問者にちょっとしたワクワク体験を届ける手順を、コード付きで解説します。

https://44310.net/blog/omikuji/

1. ゴールと概要

  • 目的:クリックするとおみくじ結果と累計回数を表示するミニアプリを固定ページに組み込む。
  • 構成
    1. functions.php … REST API(GET/POST)とスクリプト読込
    2. assets/js/omikuji.js … 抽選・描画ロジック
    3. 固定ページテンプレート … HTML と画像のみ
child-theme/
├ functions.php
└ assets/
   └ js/
      └ omikuji.js

2.functions.php(コピペ用)

/* ============================================================
 * おみくじ:REST API と JS 読み込み
 * ========================================================== */

/**
 * REST エンドポイント登録
 * – /wp-json/omikuji/v1/count  (GET)   現在値を返す
 * – /wp-json/omikuji/v1/count  (POST)  +1 して返す
 */
add_action( 'rest_api_init', function () {

    register_rest_route( 'omikuji/v1', '/count', [
        'methods'             => 'GET',
        'callback'            => function () {
            return (int) get_option( 'omikuji_total_count', 0 );
        },
        'permission_callback' => '__return_true',
    ] );

    register_rest_route( 'omikuji/v1', '/count', [
        'methods'             => 'POST',
        'callback'            => function () {
            $n = (int) get_option( 'omikuji_total_count', 0 ) + 1;
            update_option( 'omikuji_total_count', $n );
            return $n;
        },
        'permission_callback' => '__return_true',
    ] );
} );


/**
 * スクリプト(omikuji.js)を読み込む
 * - REST ルート URL を JS へ変数で渡す
 */
add_action( 'wp_enqueue_scripts', function () {

    $theme_uri  = get_stylesheet_directory_uri();                // 親でも子でも OK
    $theme_path = get_stylesheet_directory();                    // バージョンに使う

    wp_enqueue_script(
        'omikuji-js',
        $theme_uri . '/assets/js/omikuji.js',
        [],                                                      // 依存があれば ['jquery'] など
        filemtime( $theme_path . '/assets/js/omikuji.js' ),      // キャッシュバスター
        true                                                     // フッター読み込み
    );

    // PHP から JS へ値を渡す
    wp_localize_script(
        'omikuji-js',
        'omikujiVars',
        [ 'restUrl' => esc_url_raw( rest_url( 'omikuji/v1/count' ) ) ]
    );
} );

// おみくじ回数カウント
/*  functions.php など、REST リクエストでも必ず読み込まれる場所に置く  */
add_action( 'rest_api_init', 'omikuji_register_rest_routes' );

function omikuji_register_rest_routes() {

    register_rest_route( 'omikuji/v1', '/count', [
        'methods'             => 'GET',
        'callback'            => 'omikuji_get_count',
        'permission_callback' => '__return_true',
    ] );

    register_rest_route( 'omikuji/v1', '/count', [
        'methods'             => 'POST',
        'callback'            => 'omikuji_increment_count',
        'permission_callback' => '__return_true',
    ] );
}

function omikuji_get_count() {
    return (int) get_option( 'omikuji_total_count', 0 );
}

function omikuji_increment_count() {
    $n = (int) get_option( 'omikuji_total_count', 0 ) + 1;
    update_option( 'omikuji_total_count', $n );
    return $n;
}

3.omikuji.js(コピペ用)

/* global omikujiVars */

(() => {

  /* ========= おみくじデータ ========= */
  const fortunes = [
    { type: '大吉', desc: '願い事が思うままに叶い…', weight: 5,  color: '#ff5252' },
    { type: '吉',   desc: '安定しつつ上昇気流…',     weight: 15, color: '#ff8f00' },
    { type: '中吉', desc: '運気は伸び盛り…',         weight: 20, color: '#ffb300' },
    { type: '小吉', desc: '穏やかな状態…',           weight: 25, color: '#ffee58' },
    { type: '末吉', desc: '先行きはやや不透明…',     weight: 15, color: '#29b6f6' },
    { type: '凶',   desc: '注意信号が点灯…',         weight: 15, color: '#039be5' },
    { type: '大凶', desc: '試練の時期…',             weight: 5,  color: '#616161' }
  ];

  /* ========= 要素取得 ========= */
  const box      = document.getElementById('omikujiBox');
  const resWrap  = document.getElementById('result');
  const titleEl  = document.getElementById('fortuneTitle');
  const descEl   = document.getElementById('fortuneDesc');
  const retryBtn = document.getElementById('retryBtn');
  const totalEl  = document.getElementById('totalNum');
  const confetti = document.getElementById('confetti');

  /* ========= ヘルパー ========= */
  const weightedRandom = list => {
    const sum = list.reduce((a, b) => a + b.weight, 0);
    let r = Math.random() * sum;
    return list.find(f => (r -= f.weight) < 0);
  };

  const fetchTotal = inc => {
    fetch(omikujiVars.restUrl, { method: inc ? 'POST' : 'GET' })
      .then(r => {
        if (!r.ok) throw new Error(r.statusText);
        return r.json();
      })
      .then(n => { totalEl.textContent = n; })
      .catch(console.error);
  };

  const spawnConfetti = color => {
    for (let i = 0; i < 30; i++) {
      const piece = document.createElement('span');
      piece.style.background = color;
      piece.style.left = Math.random() * 100 + 'vw';
      piece.style.animationDelay = Math.random() * 0.3 + 's';
      confetti.appendChild(piece);
      setTimeout(() => piece.remove(), 2500);
    }
  };

  const showResult = f => {
    titleEl.textContent = f.type;
    descEl.textContent  = f.desc;
    titleEl.style.color = f.color;
    resWrap.classList.add('show');
    spawnConfetti(f.color);
  };

  /* ========= 初期化 ========= */
  fetchTotal(false);

  /* ========= イベント ========= */
  box.addEventListener('click', () => {
    if (box.classList.contains('shake')) return;   // 連打防止
    box.classList.add('shake');
    resWrap.classList.remove('show');
    setTimeout(() => {
      box.classList.remove('shake');
      const f = weightedRandom(fortunes);
      showResult(f);
      fetchTotal(true);
    }, 1200);
  });

  retryBtn.addEventListener('click', () => {
    resWrap.classList.remove('show');
  });

})();

4.固定ページテンプレート

<?php
/*
Template Name: おみくじ
*/
get_header();
?>

<main class="omikuji-wrap">
  <!-- おみくじ箱 -->
  <div id="omikujiBox" class="omikuji-box" title="クリックでおみくじを引く">
    <!-- 好きな画像に変更OK -->
    <img src="<?php echo esc_url( get_stylesheet_directory_uri() . '/img/omikuji.jpg' ); ?>" alt="おみくじ箱">
  </div>

  <!-- 結果 -->
  <div id="result" class="omikuji-result">
    <h2 id="fortuneTitle"></h2>
    <p id="fortuneDesc"></p>
    <button id="retryBtn" class="omikuji-btn">もう一度引く</button>
  </div>

  <!-- 累計表示 -->
  <div id="total" class="omikuji-total">累計 <span id="totalNum">0</span> 回引かれました</div>

  <!-- 紙吹雪 -->
  <div id="confetti" class="confetti"></div>
</main>
<?php get_footer(); ?>

5. 使い方

  1. 上記 3 ファイルを配置
  2. 画像 img/omikuji.jpg を用意
  3. 固定ページでテンプレート「おみくじ」を選択→公開
  4. クリックで結果表示&累計更新!

6. 仕上げのワンポイント

  • Bot 対策が必要なら wp_create_nonce('wp_rest') を使い、JS で X-WP-Nonce ヘッダーを送信。
  • 値を月次でリセットしたい場合は、option_name を年月付きにするだけでOK(例:omikuji_202506)。

これで軽量&ノンプラグインの「おみくじ」アプリは完成です。