2026年6月27日
2026年6月27日
WordPressのHeartbeat APIで管理画面にリアルタイム通知を出す実装方法
Heartbeat API を使うと、管理画面を開いたままの状態で「承認待ちコメントが増えた」「担当記事にレビュー依頼が来た」といった変化を、ページを再読み込みせずに通知できます。仕組みは一定間隔のポーリングなので厳密なリアルタイムではありませんが、プラグインや外部サーバーを増やさずに数十秒単位の通知を作るには十分です。この記事では、承認待ちコメントの増加を管理画面に表示する例を通して、JavaScript 側と PHP 側の書き方、負荷を抑える設定、動かないときの確認方法までを説明します。
Heartbeat API の仕組みを先に押さえる
Heartbeat API は WordPress 3.6 で入ったコア機能で、ブラウザが一定間隔で admin-ajax.php に action=heartbeat のリクエストを送り、サーバーがそれに応答する往復を繰り返します。投稿の自動保存や、他のユーザーが編集中であることを示す投稿ロック、ログイン切れの検知もこの仕組みに乗っています。
通知を作るときに使うのは、次の4つの接点です。
- JavaScript の
heartbeat-sendイベント:送信直前に、自分用のデータを載せる - PHP の
heartbeat_receivedフィルター:受け取ったデータを見て、応答に値を追加する - JavaScript の
heartbeat-tickイベント:応答を受け取ったときに画面を更新する - PHP の
heartbeat_settingsフィルター:送信間隔などの設定を変える
ポイントは、送るデータも返すデータも「自分のキー」で包むことです。Heartbeat の1回のリクエストには、コアや他のプラグインのデータも相乗りしています。myprefix_notify のように接頭辞付きのキーを使い、そのキーが来たときだけ処理すれば、他の機能を邪魔しません。
送信間隔は、標準ではおおむね60秒前後で、投稿編集画面などではより短くなります。タブが非アクティブになると間隔が延び、長時間操作がないと止まることもあります。細かな秒数や上限・下限はバージョンによって変わってきた経緯があるため(2026年10月現在)、実際の値はご利用のバージョンの wp-includes/js/heartbeat.js で確認してください。
作業前の準備
コードは子テーマの functions.php よりも、独自プラグインか wp-content/mu-plugins/ に置くのがおすすめです。通知機能はテーマの見た目とは関係がなく、テーマを切り替えても残したい処理だからです。いきなり本番に入れず、ステージング環境で動作を確かめてから反映してください。万一 PHP のエラーで管理画面に入れなくなっても、FTP やファイルマネージャーで該当ファイルを外せば元に戻せます。
ファイル構成の例は次のとおりです。
wp-content/plugins/my-heartbeat-notify/
├── my-heartbeat-notify.php
└── notify.js
PHP側:スクリプトの読み込みと応答の追加
まず、管理画面で heartbeat スクリプトに依存する形で自分の JavaScript を読み込みます。依存関係に heartbeat を書いておけば、読み込み順の問題で wp.heartbeat が未定義になることを防げます。読み上げ用の wp-a11y も依存に入れておくと、スクリーンリーダー利用者にも通知を伝えられます。
<?php
/**
* Plugin Name: My Heartbeat Notify
* Description: 承認待ちコメントの増加を管理画面で通知する
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'admin_enqueue_scripts', function () {
// コメントを承認できるユーザーにだけ読み込む
if ( ! current_user_can( 'moderate_comments' ) ) {
return;
}
wp_enqueue_script(
'my-heartbeat-notify',
plugins_url( 'notify.js', __FILE__ ),
array( 'heartbeat', 'jquery', 'wp-a11y' ),
'1.0.0',
true
);
wp_localize_script(
'my-heartbeat-notify',
'myHeartbeatNotify',
array(
'initial' => (int) wp_count_comments()->moderated,
'message' => __( '承認待ちのコメントが %d 件あります。', 'my-heartbeat-notify' ),
)
);
} );
add_filter( 'heartbeat_received', function ( $response, $data, $screen_id ) {
// 自分のキーが無いリクエストには何もしない
if ( empty( $data['myprefix_notify'] ) ) {
return $response;
}
// Heartbeat 自体はログイン中なら誰でも送れるため、権限を確認する
if ( ! current_user_can( 'moderate_comments' ) ) {
return $response;
}
$response['myprefix_notify'] = array(
'pending' => (int) wp_count_comments()->moderated,
);
return $response;
}, 10, 3 );
heartbeat_received の引数は、応答の配列、ブラウザから届いたデータ、画面 ID の3つです。ログイン中ユーザーの Heartbeat には専用の nonce がコア側で付いて検証されますが、それは「そのユーザー本人のリクエストか」を確かめるものです。「そのユーザーに見せてよい情報か」は別問題なので、上のように current_user_can() で権限を確認してから値を返します。
ここで wp_count_comments() を使っているのは、結果がキャッシュされる軽い関数だからです。Heartbeat の処理は、管理画面を開いている全員の、全タブから定期的に呼ばれます。重い WP_Query や外部 API の呼び出しをここに入れると、閲覧者が増えたときにサーバーの負荷として表に出ます。重い集計が必要なら、WP-Cron などで事前に計算してトランジェントに保存し、Heartbeat ではそれを読むだけにする形が安全です。
JavaScript側:送信データと受信時の表示
次に notify.js です。送信時にキーを載せ、応答に自分のキーがあれば前回の件数と比べて、増えていたら通知を出します。
( function ( $ ) {
var settings = window.myHeartbeatNotify || {};
var lastCount = parseInt( settings.initial, 10 ) || 0;
$( document ).on( 'heartbeat-send', function ( event, data ) {
data.myprefix_notify = { since: lastCount };
} );
$( document ).on( 'heartbeat-tick', function ( event, data ) {
if ( ! data.myprefix_notify ) {
return;
}
var count = parseInt( data.myprefix_notify.pending, 10 ) || 0;
if ( count > lastCount ) {
showNotice( settings.message.replace( '%d', count ) );
}
lastCount = count;
} );
function showNotice( text ) {
var $notice = $( '#myprefix-notify-notice' );
if ( ! $notice.length ) {
$notice = $( '<div id="myprefix-notify-notice" class="notice notice-info is-dismissible"><p></p></div>' );
$( '#wpbody-content' ).prepend( $notice );
}
// text() で入れることで HTML として解釈させない
$notice.find( 'p' ).text( text );
if ( window.wp && wp.a11y && wp.a11y.speak ) {
wp.a11y.speak( text );
}
}
} )( jQuery );
表示には html() ではなく text() を使っています。今回の値は数値だけですが、将来コメント本文の抜粋や投稿タイトルを通知に含めるようになったとき、HTML として挿入すると XSS の入り口になります。最初から「文字列として入れる」書き方にしておくと安心です。
通知の見た目は管理画面標準の notice クラスを使っているので、追加の CSS は要りません。件数が減ったとき(誰かが承認した)は通知を出さず、lastCount だけ更新しています。何を通知の対象にするかは、ここで条件を変えれば調整できます。
送信間隔を調整する
通知をもっと早く出したい場合は、heartbeat_settings フィルターで間隔を短くできます。
add_filter( 'heartbeat_settings', function ( $settings ) {
// 秒単位。短くするほどサーバーへのリクエストが増える
$settings['interval'] = 30;
return $settings;
} );
ただし、間隔を半分にするとリクエスト数はおよそ2倍です。Heartbeat の1回1回は WordPress 全体を読み込む Ajax リクエストなので、共用サーバーでは CPU 使用率の制限に触れる原因になりがちです。管理画面を同時に開く人数とタブ数を考えて、必要な画面だけに絞るのが現実的です。JavaScript 側の wp.heartbeat.interval() で一時的に間隔を変えることもできますが、受け付ける値の範囲はバージョンで異なるため、使う前にソースを確認してください。
反対に、パフォーマンス系のプラグインで Heartbeat を止めたり間隔を延ばしたりしていると、この通知も止まります。キャッシュ・高速化プラグインの Heartbeat 制御設定や、Heartbeat Control のような専用プラグインを入れている場合は、通知を出したい画面で Heartbeat が有効になっているかを確かめてください。
動かないときの確認手順
うまく表示されないときは、上から順に確認すると原因を絞り込みやすくなります。
- スクリプトが読み込まれているか:ブラウザの開発者ツールの「ネットワーク」で
notify.jsが読み込まれているか見ます。読み込まれていなければ、権限チェックで弾かれているか、プラグインが有効化されていません。 - Heartbeat のリクエストが出ているか:ネットワークで
admin-ajax.phpを絞り込み、ペイロードにaction=heartbeatとmyprefix_notifyが含まれているか確認します。リクエスト自体が無い場合は、Heartbeat を止める設定やプラグインを疑います。 - 応答に自分のキーがあるか:同じリクエストのレスポンス JSON に
myprefix_notifyがあるか見ます。無ければ PHP 側のフィルターが動いていないか、権限チェックで返していません。 - PHP エラーが出ていないか:
wp-config.phpでWP_DEBUGとWP_DEBUG_LOGを有効にし、wp-content/debug.logを確認します。本番ではWP_DEBUG_DISPLAYを false にして画面には出さないようにしてください。 - 待ち時間が足りているか:タブが背面にあると間隔が延びます。テストするときはタブを前面にしたまま、1〜2分待ってから判断します。
フロントエンドで使う場合の注意
Heartbeat は管理画面向けの機能ですが、wp_enqueue_scripts で heartbeat を依存に指定すればフロントエンドでも動きます。ログインしていない訪問者向けには heartbeat_nopriv_received フィルターが用意されています。
ただし、未ログインの訪問者全員に定期リクエストを送らせると、ページキャッシュが効かない Ajax リクエストが訪問者数に比例して増えます。アクセスの多いサイトでは負荷の面で現実的ではありません。また nopriv 側には本人確認がないので、返してよいのは誰に見られても問題のない公開情報だけです。
Heartbeat が向かないケース
Heartbeat はあくまでポーリングなので、チャットのように数秒以内の双方向のやり取りが必要な用途には向きません。その場合は WebSocket や Server-Sent Events を使う構成を検討します。一方で、「数十秒遅れても困らない」「管理画面を開いているログインユーザーだけが対象」という通知なら、追加のサーバーや常駐プロセスが要らない Heartbeat が最も手軽です。ブラウザを閉じていても届けたい通知は、メールや Web プッシュ通知の領域になります。
まとめ
- Heartbeat API は
heartbeat-sendでデータを載せ、heartbeat_receivedで応答を足し、heartbeat-tickで受け取る往復で動く - 送受信のデータは接頭辞付きの自分のキーで包み、他の機能と混ざらないようにする
- 応答を返す前に
current_user_can()で権限を確認し、表示はtext()で文字列として挿入する - Heartbeat の処理は全ユーザー・全タブから呼ばれるため、重い処理を入れず、間隔の短縮は慎重に行う
- 動かないときは、スクリプトの読み込み、リクエストの有無、応答のキー、PHP エラーの順に確認する