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(サーバー内だけ)──┘
  1. ブラウザーは、ページを開いたときに wss://example.com/ws/ へ WebSocket で接続します。
  2. Nginx が /ws/ への接続を Node.js のサーバーへ中継します。
  3. WordPress で記事が公開されると、プラグインが同じサーバー内の Node.js へ HTTP で通知します。
  4. Node.js が、接続中のすべてのブラウザーへメッセージを送ります。

この構成が使えない環境

Node.js のプロセスを常に動かしておく必要があるため、VPS・クラウドサーバー・専用サーバーのように root 権限か常駐プロセスを置ける環境が前提です。一般的な共用レンタルサーバーでは使えないことが多いので、その場合は次のどちらかを検討します。

ステップ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: 動作を確かめる

  1. サイトの公開ページをブラウザーで開き、開発者ツールの「ネットワーク」で ws/ への接続が 101 Switching Protocols になっていることを確かめます。
  2. サーバー上で、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 による切断検知を入れ、送る内容は公開してよい情報に限ります。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲