2026年9月17日
2026年9月17日
WordPressでWebSocketのリアルタイム通知を実装する|Node.jsの中継サーバーとNginx設定
チャットや「新しい記事が公開されました」のような即時の通知を WordPress サイトに付けたいとき、候補になるのが WebSocket です。WebSocket はブラウザーとサーバーの間の接続を開いたままにし、サーバー側から好きなときにデータを送れる通信方式です。
ただし、WordPress は PHP がリクエストごとに動いて終わる仕組みなので、WordPress そのものを WebSocket サーバーにすることはできません。この記事では、WebSocket の接続を受け持つ小さな Node.js のサーバーを WordPress の横に置き、記事の公開をきっかけにブラウザーへ通知する仕組みを作ります。コードは Node.js 用ライブラリー ws(2026年10月時点の 8 系)で動作を確かめています。
全体の構成
[ブラウザー] ──wss://example.com/ws/──> [Nginx] ──> [Node.js (ws) 127.0.0.1:8080]
↑
[WordPress] ──記事公開時に HTTP POST /notify(サーバー内だけ)──┘
- ブラウザーは、ページを開いたときに
wss://example.com/ws/へ WebSocket で接続します。 - Nginx が
/ws/への接続を Node.js のサーバーへ中継します。 - WordPress で記事が公開されると、プラグインが同じサーバー内の Node.js へ HTTP で通知します。
- Node.js が、接続中のすべてのブラウザーへメッセージを送ります。
この構成が使えない環境
Node.js のプロセスを常に動かしておく必要があるため、VPS・クラウドサーバー・専用サーバーのように root 権限か常駐プロセスを置ける環境が前提です。一般的な共用レンタルサーバーでは使えないことが多いので、その場合は次のどちらかを検討します。
- WordPress 本体の Heartbeat API で、一定間隔でサーバーに問い合わせる(Heartbeat APIでリアルタイム通知を作る方法)
- WebSocket の配信を代行する外部サービスを使う
ステップ1: Node.js の WebSocket サーバーを作る
サーバーに Node.js と npm を入れたうえで、作業用のフォルダーを作り ws を入れます。
sudo mkdir -p /opt/ws-notify
sudo chown "$USER" /opt/ws-notify
cd /opt/ws-notify
npm init -y
npm install ws
/opt/ws-notify/server.mjs を次の内容で作ります。ALLOWED_ORIGIN は自分のサイトの URL に変えてください。
import { createServer } from 'node:http';
import WebSocket, { WebSocketServer } from 'ws';
const PORT = 8080;
const TOKEN = process.env.NOTIFY_TOKEN;
const ALLOWED_ORIGIN = 'https://example.com';
if ( ! TOKEN ) {
console.error( 'NOTIFY_TOKEN が設定されていません' );
process.exit( 1 );
}
const wss = new WebSocketServer( { noServer: true } );
// 接続中のすべてのブラウザーへ送る
function broadcast( message ) {
const data = JSON.stringify( message );
wss.clients.forEach( ( client ) => {
if ( client.readyState === WebSocket.OPEN ) {
client.send( data );
}
} );
}
// WordPress からの通知を受ける(127.0.0.1 だけで待ち受ける)
const server = createServer( ( req, res ) => {
if ( req.method === 'POST' && req.url === '/notify' ) {
if ( req.headers[ 'x-notify-token' ] !== TOKEN ) {
res.writeHead( 403 ).end();
return;
}
let body = '';
req.on( 'data', ( chunk ) => {
body += chunk;
if ( body.length > 10000 ) {
req.destroy();
}
} );
req.on( 'end', () => {
try {
broadcast( JSON.parse( body ) );
res.writeHead( 204 ).end();
} catch {
res.writeHead( 400 ).end();
}
} );
return;
}
res.writeHead( 404 ).end();
} );
// ブラウザーからの WebSocket 接続(/ws/ と自サイトの Origin だけ受け付ける)
server.on( 'upgrade', ( request, socket, head ) => {
if ( request.url !== '/ws/' || request.headers.origin !== ALLOWED_ORIGIN ) {
socket.write( 'HTTP/1.1 403 Forbidden\r\n\r\n' );
socket.destroy();
return;
}
wss.handleUpgrade( request, socket, head, ( ws ) => {
wss.emit( 'connection', ws, request );
} );
} );
// 応答のない接続を30秒ごとに切る
wss.on( 'connection', ( ws ) => {
ws.isAlive = true;
ws.on( 'error', console.error );
ws.on( 'pong', () => {
ws.isAlive = true;
} );
} );
const interval = setInterval( () => {
wss.clients.forEach( ( ws ) => {
if ( ws.isAlive === false ) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
} );
}, 30000 );
wss.on( 'close', () => clearInterval( interval ) );
server.listen( PORT, '127.0.0.1' );
このサーバーの要点は次のとおりです。
127.0.0.1だけで待ち受けるので、インターネットから直接は届きません。外からの接続は Nginx 経由の/ws/だけです。/notifyはX-Notify-Tokenヘッダーが合うときだけ受け付けます。合言葉は環境変数NOTIFY_TOKENで渡します。- WebSocket の接続は、
Originヘッダーが自分のサイトのときだけ受け付けます。ほかのサイトのページから勝手に接続されるのを防ぎます。 - 30秒ごとに ping を送り、応答のない接続を切ります。
wsの説明書にある方法です。
ステップ2: systemd で常駐させる
合言葉を書いた環境ファイルを作ります。値は長いランダムな文字列にします。
openssl rand -hex 32
sudo tee /etc/ws-notify.env > /dev/null <<'EOF'
NOTIFY_TOKEN=ここに上のコマンドで作った文字列
EOF
sudo chmod 600 /etc/ws-notify.env
/etc/systemd/system/ws-notify.service を作ります。node の場所は which node で確かめて合わせてください。
[Unit]
Description=WebSocket notify server for WordPress
After=network.target
[Service]
WorkingDirectory=/opt/ws-notify
ExecStart=/usr/bin/node server.mjs
EnvironmentFile=/etc/ws-notify.env
Restart=always
User=nobody
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ws-notify
sudo systemctl status ws-notify
ステップ3: Nginx で /ws/ を中継する
WebSocket は、HTTP の接続を Upgrade ヘッダーで切り替えて始まります。Nginx の公式ドキュメントにあるとおり、このヘッダーは中継先へ自動では渡らないので、明示的に渡します。サイトの server ブロック(HTTPS 側)に次を加えます。
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 120s;
}
sudo nginx -t
sudo systemctl reload nginx
proxy_http_version 1.1は、公式ドキュメントでは Nginx 1.29.7 より前で必要とされています。付けておいても問題ありません。- Nginx は、中継先から60秒間データが来ないと既定で接続を閉じます。サーバーが30秒ごとに ping を送るので切れませんが、
proxy_read_timeoutで余裕を持たせています。 proxy_passの URL の末尾にパスを付けていないので、Node.js には/ws/のまま届きます。
Apache を使っている場合は、mod_proxy_wstunnel などで同じ中継を設定します。
ステップ4: WordPress のプラグインを作る
まず wp-config.php の「編集が必要なのはここまでです」の行より上に、ステップ2と同じ合言葉を定数で書きます。
define( 'WS_NOTIFY_TOKEN', 'ここにステップ2で作った文字列' );
wp-content/plugins/realtime-notice/realtime-notice.php を作ります。
<?php
/**
* Plugin Name: Realtime New Post Notice
* Description: 記事の公開を WebSocket で閲覧中のブラウザーに知らせる。
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 記事が初めて公開されたときに Node.js へ通知する
add_action(
'transition_post_status',
function ( $new_status, $old_status, $post ) {
if ( 'publish' !== $new_status || 'publish' === $old_status || 'post' !== $post->post_type ) {
return;
}
if ( ! defined( 'WS_NOTIFY_TOKEN' ) ) {
return;
}
wp_remote_post(
'http://127.0.0.1:8080/notify',
array(
'timeout' => 2,
'blocking' => false,
'headers' => array(
'Content-Type' => 'application/json',
'X-Notify-Token' => WS_NOTIFY_TOKEN,
),
'body' => wp_json_encode(
array(
'type' => 'new_post',
'title' => wp_strip_all_tags( get_the_title( $post ) ),
'url' => get_permalink( $post ),
)
),
)
);
},
10,
3
);
// 公開ページに接続用の JavaScript を読み込む
add_action(
'wp_enqueue_scripts',
function () {
wp_enqueue_script(
'realtime-notice',
plugins_url( 'realtime-notice.js', __FILE__ ),
array(),
'1.0.0',
array( 'in_footer' => true )
);
wp_add_inline_script(
'realtime-notice',
'const realtimeNotice = ' . wp_json_encode( array( 'url' => 'wss://example.com/ws/' ) ) . ';',
'before'
);
}
);
transition_post_statusは投稿のステータスが変わるたびに呼ばれるフックです。「公開以外から公開へ」変わったときだけ通知し、公開済みの記事の更新では通知しません。'blocking' => falseにして、Node.js の応答を待たずに保存処理を続けます。Node.js が止まっていても記事の保存は失敗しません。
同じフォルダーに realtime-notice.js を作ります。
( function () {
if ( ! window.WebSocket ) {
return;
}
let retry = 1000;
function connect() {
const socket = new WebSocket( realtimeNotice.url );
socket.addEventListener( 'open', () => {
retry = 1000;
} );
socket.addEventListener( 'message', ( event ) => {
const data = JSON.parse( event.data );
if ( data.type !== 'new_post' ) {
return;
}
const box = document.createElement( 'div' );
box.className = 'realtime-notice';
const link = document.createElement( 'a' );
link.href = data.url;
link.textContent = '新しい記事: ' + data.title;
box.appendChild( link );
document.body.appendChild( box );
} );
// 切れたら待ち時間を延ばしながら再接続する(最大30秒)
socket.addEventListener( 'close', () => {
setTimeout( connect, retry );
retry = Math.min( retry * 2, 30000 );
} );
}
connect();
} )();
題名は textContent で入れているので、題名に HTML が含まれていてもタグとして実行されません。表示位置や見た目は、.realtime-notice にテーマの CSS で指定します。
管理画面の「プラグイン」で「Realtime New Post Notice」を有効化します。
ステップ5: 動作を確かめる
- サイトの公開ページをブラウザーで開き、開発者ツールの「ネットワーク」で
ws/への接続が101 Switching Protocolsになっていることを確かめます。 - サーバー上で、WordPress を通さずに通知を送ってみます。
NOTIFY_TOKEN=$(sudo sed -n 's/^NOTIFY_TOKEN=//p' /etc/ws-notify.env)
curl -i -X POST http://127.0.0.1:8080/notify \
-H "X-Notify-Token: $NOTIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"new_post","title":"テスト","url":"https://example.com/"}'
204 No Content が返り、開いているページに「新しい記事: テスト」が出れば、Node.js と Nginx の設定は正しく動いています。
最後に、WordPress で記事を1つ公開し、開いているページに通知が出ることを確かめます。
うまく動かないとき
| 症状 | 確かめること |
|---|---|
| ブラウザーの接続が 403 になる | server.mjs の ALLOWED_ORIGIN が、ブラウザーのアドレス欄のオリジン(https:// とドメイン、www の有無)と一致しているか |
| 接続が 400・502 になる | systemctl status ws-notify でサーバーが動いているか。Nginx の Upgrade・Connection の設定が入っているか |
| 接続は成功するが通知が来ない | curl での通知が 204 になるか。wp-config.php の WS_NOTIFY_TOKEN と環境ファイルの値が同じか |
| 約1分ごとに接続が切れる | ping の処理が動いているか。Nginx の proxy_read_timeout が短すぎないか |
| サーバーのログを見たい | journalctl -u ws-notify -f で Node.js の出力を追う |
運用上の注意
- 公開してよい情報だけを送る: この例はすべての閲覧者に同じ内容を送るので、記事の題名と URL のような公開情報だけにしています。ログイン中のユーザーごとに違う内容(個人宛ての通知やチャット)を送る場合は、接続時に WordPress で発行した短時間有効のトークンを確かめるなど、ユーザーを認証する仕組みが別に必要です。
- HTTPS のサイトでは
wss://を使う: HTTPS のページから暗号化されていないws://へは接続できません。この構成では、Nginx が HTTPS と同じ証明書でwss://を受けます。 - 接続数とメモリー: 閲覧者1人につき1本の接続が開いたままになります。アクセスの多いサイトでは、
ss -tnなどで接続数とメモリーの使用量を見ながら運用します。 - 複数台構成: Web サーバーを複数台にする場合、Node.js のサーバーも台数分になり、通知がすべての台に届く仕組み(共有のメッセージ配信など)が別に必要になります。
まとめ
- WordPress はリクエストごとに PHP が動いて終わる仕組みで WebSocket の常時接続を保てないので、接続は Node.js などの別のサーバーに任せます。
- WordPress は
transition_post_statusなどのフックで、サーバー内の Node.js に HTTP で知らせるだけにします。 - Nginx では
/ws/にUpgradeとConnectionヘッダーを渡して中継し、Node.js は127.0.0.1だけで待ち受けます。 - 通知用の合言葉、
Originの確認、ping による切断検知を入れ、送る内容は公開してよい情報に限ります。