Service WorkerとPHPで実装するWeb Push通知:最小構成で構築する入門ガイド

Web Push通知は、ユーザーがサイトを閉じていても情報を届けられる仕組みで、再訪のきっかけ作りや重要なお知らせの即時配信など幅広い用途に活用できます。かつてはネイティブアプリでしか実現できなかったプッシュ通知が、Service Worker と Push API の登場によりウェブでも可能になりました。本記事はWeb Push通知の入門者向けの導入ガイドとして、Service Worker と PHP(Minishlink/web-push)を用い、VAPID鍵の生成 → 購読データの保存 → 管理画面からの通知送信 までを一気通貫で最小構成として構築します。本番運用時には無効購読の整理・解除導線の設計・セグメント配信・レート制御・ログ監視・プライバシー設計なども考慮する必要がありますが、まずは最小構成でWeb Push通知のコアとなる部分を構築し、段階的に拡張していきましょう。

概要と仕組みの理解

Web Push通知は、ユーザーのブラウザに直接メッセージを届けられる仕組みで、アプリのプッシュ通知と同等の効果をウェブで実現できます。その基本構成は以下の3要素に分かれます。

  • Service Worker:ブラウザのバックグラウンドで動作し、Pushイベントを受け取り通知を表示します。
  • Push API:ブラウザとプッシュサービス(ブラウザベンダーが提供するサーバー)をつなぎ、購読の作成や管理を行います。
  • アプリケーションサーバー(PHP側):購読情報を保存し、VAPID鍵を使って署名したリクエストをPushサービスへ送信します。

通知の流れは次の通りです(登場人物:フロントエンド(html & Javascript)Service Worker(Javascript)アプリサーバー(PHP & DB)プッシュサービス(ブラウザベンダー))。

  1. ServiceWorker登録(フロント
    ページ読み込み時に navigator.serviceWorker.register('/sw.js') をなど実行し、ブラウザ内にバックグラウンド処理の土台であるServiceWorkerを登録します。
  2. 通知許可のリクエスト(フロント
    ユーザー操作(クリックなど)に紐づけてNotification.requestPermission() を実行し、許可(granted)になって初めて購読を作成します。
  3. 購読(Subscription)の作成(フロント→ServiceWorker)
    フロントからServiceWorkerのreg.pushManager.subscribe() を呼び出し、購読情報を取得します。戻り値には endpoint(送信先URL)と keys.p256dh / keys.auth(暗号化用鍵)が含まれます。
  4. 購読情報をサーバーへ送信(フロント→PHP)
    取得した 購読情報を アプリサーバーに送信します。ユーザーIDやセグメント情報と関連付ける場合はここで同時に送ります。
  5. 購読情報の保存(PHP→DB)
    PHP側で取得した購読情報をサーバー内に保存します。データの保存が出来ればDBである必要はありませんが、今回の例ではendpoint を主キー相当で一意に管理し、p256dhauth とともDBに保存します。
  6. 通知リクエストの作成と署名(PHP→プッシュサービス)
    送信時、PHP(Minishlink/web-push)が VAPIDの秘密鍵 でリクエストを署名し、メッセージ本文を Web Push プロトコルで暗号化して、各ブラウザのプッシュサービス(例:FCM, Mozilla Autopush など)に送信します。
  7. プッシュサービスから端末へ配信(プッシュサービス→ServiceWorker)
    プッシュサービスはサーバーから受け取ったメッセージを保持し、対象端末がオンラインになったタイミングで配送します(TTL 超過や配信失敗時は破棄される場合があります)。
  8. Service Workerで受信・表示(ServiceWorker
    端末側の ServiceWorkerが push イベントでペイロードを受け取り、ユーザーに通知を表示します。
  9. 通知クリックのハンドリング(ServiceWorker
    notificationclick イベントで、通知のクリック後の動作を制御します。既存タブの起動確認とフォーカス制御や、新規タブでのページ遷移など。
  10. サブスクリプションの失効とメンテナンス(フロント/ServiceWorkerPHP)
    送信時に 410 Gone などが返れば、その購読は無効と判断してDBから削除したり、フロントでは起動時に reg.pushManager.getSubscription() で状態確認し、無ければ再購読を促したり、ユーザーのオプトアウト(subscription.unsubscribe())も設計に含める必要があります。本記事では通知の送信までをターゲットにしておりますので、メンテナンスの処理に関しては実際のプロジェクトに応じて設計してください。

この全体像を理解しておくことで、以降の実装ステップがスムーズになります。


ディレクトリ構成(例)

本記事でのディレクトリ構成を示します。あくまでもWeb Push通知の動作確認用のサンプルですので、実際のプロジェクトに併せて変更してください。「api」や「admin」などは別サーバーなどよりセキュアな環境にて実現する事が推奨されますが、本記事では説明の便宜上ドキュメントルート配下に設置しています。

project/
├─ public_html/ # サイトのドキュメントルート
│ ├─ index.html # フロントの購読登録ページ
│ ├─ sw.js # Service Worker
│ ├─ api/ # API 用ディレクトリ
│ │ └─ save-subscription.php # 購読保存処理
│ └─ admin/ # 管理者用(Basic認証やIP制限を推奨)
│  ├─ send.html # 通知送信用フォーム
│  └─ send.php # 通知送信処理
├─ keys/
│ ├─ vapid_public.key
│ └─ vapid_private.key
└─ vendor/ # composer ライブラリ

1. PHPライブラリの導入

Web Push通知をPHPから送信するには、暗号化処理やVAPID署名などの複雑な手続きを行う必要があります。これを自前で実装するのは大変なので、実績のあるライブラリ Minishlink/web-push を利用します。

Composer を使ってインストールします。プロジェクトルートで以下を実行してください。

composer require minishlink/web-push

インストール後は vendor/ ディレクトリにライブラリが展開され、autoload.php から読み込めるようになります。サーバー側のPHPスクリプトでは次のように読み込みます。

<?php
require __DIR__ . '/../vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

この準備により、PHPから安全にPush通知を送信する基盤が整いました。


2. VAPID鍵の作成

Push通知を正しく送信するためには、サーバー側が「この通知は正規の発行者から送られている」と証明する必要があります。その仕組みが VAPID (Voluntary Application Server Identification) です。VAPIDは公開鍵・秘密鍵のペアを生成して、通知送信リクエストに署名するために使います。

鍵の生成方法

VAPID鍵はMinishlink\WebPush\VAPID::createVapidKeys() を利用してPHPから直接生成できます。
keysディレクトリ内に以下のPHPプログラム(generate_vapid.php)を設置し、実行すると公開鍵(vapid_public.key)と秘密鍵(vapid_private.key)のそれぞれが生成されます。

<?php
// keys/generate_vapid.php
require __DIR__ . '/../vendor/autoload.php';

use Minishlink\WebPush\VAPID;

$keys = VAPID::createVapidKeys();

// 生成結果
$public  = $keys['publicKey'];
$private = $keys['privateKey'];

echo "Public : {$public}";
echo "Private: {$private}";

// ファイルに保存(既存ファイルがある場合は上書きしない運用がおすすめ)
file_put_contents(__DIR__ . '/vapid_public.key', $public);
file_put_contents(__DIR__ . '/vapid_private.key', $private);

echo "Saved to keys/vapid_public.key and keys/vapid_private.key";
% cd keys/
% php generate_vapid.php
keys/
├── vapid_public.key   # 公開鍵(フロントや購読生成時に利用)
└── vapid_private.key  # 秘密鍵(サーバーからの送信時に利用)
  • 公開鍵はクライアント側の JavaScript に埋め込む形で利用します。
  • 秘密鍵はサーバー側(PHPのsend処理)でのみ利用し、絶対に外部公開しないよう注意します。

セキュリティ上、keys/ は必ず公開ディレクトリ(public_html/)の外に置いてください。これらの値は毎回生成するものではなく、長期間使い続けるものです。鍵を更新すると購読が無効化されまてしまいますのでご注意ください。

generate_vapid.phpは鍵の生成が完了したら以降は使う事はありませんので削除して問題ありません。


3. 購読管理用 DB の作成と保存用PHPプログラム

ここからは、実際に購読情報をサーバーに保存する仕組みを整えます。購読情報はjsonなどのテキストファイルに保存する事も可能ですが、今回の例では将来的な拡張性や操作性を考慮し、データベースに登録する形で実装します。push_subscriptions というテーブルを用意し、endpoint をユニークに管理することで、同一端末の再購読や重複登録を防ぎます。また、将来的なセグメント配信(特定のユーザーのみに配信)に備えて user_id というフィールドを持たせています。

テーブル作成SQL(MySQL想定)

CREATE TABLE push_subscriptions (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  user_id VARCHAR(32) NULL,
  endpoint TEXT NOT NULL,
  p256dh VARCHAR(255) NOT NULL,
  auth VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uniq_endpoint (endpoint(191)),
  INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

設計ポイント

  • user_id: 任意。会員と購読をひも付けたい場合に利用します(匿名ならNULL)。
  • endpoint: ブラウザのプッシュサービス上の送信先URL。長いURLになるため TEXT を採用し、ユニーク制約は プレフィックスインデックス (191) で付与しています。
  • p256dh / auth: メッセージ暗号化に必要な鍵。長さは実務上 255 で十分です。
  • created_at / updated_at: 監査・メンテ用。失効判定や最終更新のトラッキングに使用。

文字コードは utf8mb4、タイムゾーンはアプリ側で統一管理するのがおすすめです。InnoDB を前提とします。

購読情報を保存するAPI(INSERT/UPDATE)

同一 endpoint の重複を避けるため、UPSERT(重複時は更新)で保存します。DBの情報(DBホスト、DB名、ユーザー名、パスワード)は環境に併せて設定してください。

<?php
// public_html/api/save-subscription.php

header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  http_response_code(405);
  echo json_encode(['error' => 'Method Not Allowed']);
  exit;
}

$raw = file_get_contents('php://input');
$input = json_decode($raw, true);
if (!is_array($input)) {
  http_response_code(400);
  echo json_encode(['error' => 'Invalid JSON']);
  exit;
}

// {subscription: {...}} 形式を想定
$sub = $input['subscription'];
$endpoint = $sub['endpoint']       ?? '';
$p256dh   = $sub['keys']['p256dh'] ?? '';
$auth     = $sub['keys']['auth']   ?? '';
$userId   = $sub['user_id'] ?? null;

if (!$endpoint || !$p256dh || !$auth) {
  http_response_code(400);
  echo json_encode(['error' => 'Missing required fields (endpoint, keys.p256dh, keys.auth)']);
  exit;
}

// ====== DB接続情報をあなたの環境に合わせてください ======
$dsn  = 'mysql:host=localhost;dbname=push_demo;charset=utf8mb4';
$user = 'user';
$pass = 'password';

try {
  $pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);

  $sql = <<<SQL
INSERT INTO push_subscriptions (user_id, endpoint, p256dh, auth)
VALUES (:uid, :e, :p, :a)
ON DUPLICATE KEY UPDATE
  user_id   = VALUES(user_id),
  p256dh    = VALUES(p256dh),
  auth      = VALUES(auth),
  updated_at = NOW()
SQL;

  $stmt = $pdo->prepare($sql);
  $stmt->execute([
    ':uid' => $userId,   // nullでもOK
    ':e'   => $endpoint,
    ':p'   => $p256dh,
    ':a'   => $auth,
  ]);

  echo json_encode(['ok' => true]);

} catch (Throwable $e) {
  http_response_code(500);
  echo json_encode(['error' => $e->getMessage()]);
}

4. Service Worker の準備

購読した端末に通知を表示するためには Service Worker が必要です。ブラウザのバックグラウンドで常駐し、push イベントを受けてユーザーに通知を出します。

Service Workerのサンプル(public_html/sw.js)

// sw.js

// install:SWが初回インストールされるとき。キャッシュ生成や初期化。Web Pushの動作確認には不要のため今回は省略。
self.addEventListener("install", (event) => {
});

// activate:新しいSWが有効化されるとき。古いキャッシュの削除など。Web Pushの動作確認には不要のため今回は省略。
self.addEventListener("activate", (event) => {
});

// message:ページ側からのメッセージ(状態共有など)を受け取った時。Web Pushの動作確認には不要のため今回は省略。
self.addEventListener("message", (event) => {
});

// fetch:リクエストされたファイルのキャッシュ戦略の定義など。Web Pushの動作確認には不要のため今回は省略。
self.addEventListener("fetch", (event) => {
});

// push:サーバーからのWeb Push を受信した場合の処理。
self.addEventListener('push', event => {
  const data = event.data ? event.data.json() : {};
  const title = data.title || '新着通知';
  const options = {
    body: data.body || "",
    icon: "/icon-192x192.png", // 公開配下にデフォルトのアイコンを用意
    data: data.data || {}, // クリック時に参照する任意データ({ url: "...", ... } など)
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

// notificationclick:通知がクリックされたとき。該当URLを開く/既存タブをフォーカスなどを定義。
self.addEventListener('notificationclick', event => {
  event.notification.close();
  const targetUrl = event.notification.data.url;

  // 非同期処理(ウィンドウの検索やオープン)が終わるまで SWの寿命を延長。
  event.waitUntil(

    // SW のスコープ配下にあるタブ(Window Client)一覧を取得。
    clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => {

      // すでに開いているタブの中から URLが完全一致するものを見つけたら そのタブにフォーカス。
      for (const client of clientList) {
        if (client.url === targetUrl && 'focus' in client) {
          return client.focus();
        }
      }

      // 一致タブが無ければ 新しいタブを開く。
      if (clients.openWindow) {
        return clients.openWindow(targetUrl);
      }
    })
  );
});

// notificationclose:通知を閉じたとき。ログ送信など。今回は省略。
self.addEventListener('notificationclose', event => {
});

このコードで 通知の表示クリック時の遷移 を処理できます。


5. フロントエンドで購読を取得・送信する

フロントエンドではユーザーに通知許可を求め、購読(Subscription)を作成し、サーバーへ送信します。公開鍵(VAPID公開鍵)を利用して購読を生成します。

通知許可を求めるページ(public_html/index.html)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Web Push デモ</title>
</head>
<body>
  <h1>Web Push 通知</h1>
  <p>「通知を有効化」を押して購読を登録します(HTTPS必須)。</p>
  <button id="enable-push">通知を有効化</button>

  <script>
    // publicKey を貼り付け
    const VAPID_PUBLIC_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

    // URL Safe Base64 → Uint8Array
    function urlBase64ToUint8Array(base64String) {
      const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
      const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
      const rawData = atob(base64);
      return Uint8Array.from([...rawData].map(c => c.charCodeAt(0)));
    }

    async function enablePush() {
      if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
        alert("このブラウザはPush通知に対応していません。");
        return;
      }

      // Service Worker を登録
      const reg = await navigator.serviceWorker.register("/sw.js");

      // 通知許可を取得
      const perm = await Notification.requestPermission();
      if (perm !== "granted") {
        alert("通知が許可されませんでした。");
        return;
      }

      // 既存購読があれば再利用、なければ新規購読
      let sub = await reg.pushManager.getSubscription();
      if (!sub) {
        sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
        });
      }

      // --- 将来的に様々パラメータ(ver情報やmeta情報)を送信できるようにsubscription でラップして送信 ---
      const payload = {
        subscription: sub,
        // 任意で追加可能(必要ならサーバ側で受け付ける実装を)
        // v: 1,
        // type: "webpush",
        // meta: { lang: navigator.language, tz: Intl.DateTimeFormat().resolvedOptions().timeZone }
      };

      const res = await fetch("/api/save-subscription.php", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });

      if (!res.ok) {
        const t = await res.text().catch(() => "");
        throw new Error(`購読保存に失敗: ${res.status} ${t}`);
      }

      alert("通知の購読が完了しました。");
    }

    document.getElementById("enable-push")?.addEventListener("click", () => {
      enablePush().catch(err => {
        console.error(err);
        alert("エラー: " + err.message);
      });
    });
  </script>
</body>
</html>

6. 管理画面から通知を送信する

管理者が通知を送るためのシンプルな仕組みを作成します。フォームから入力した内容を send.php にPOSTし、購読情報へ一斉送信します。

管理画面HTML(public_html/admin/send.html)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Web Push 送信</title>
  <style>
    :root { color-scheme: light dark; }
    body { font-family: system-ui, -apple-system, sans-serif; margin: 24px; max-width: 720px; }
    h1 { margin: 0 0 16px; }
    fieldset { border: 1px solid #ccc; border-radius: 8px; padding: 16px; margin: 16px 0; }
    label { display: block; margin: 8px 0 4px; font-weight: 600; }
    input[type="text"], input[type="url"], input[type="number"], textarea {
      width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 8px; box-sizing: border-box;
    }
    textarea { min-height: 100px; resize: vertical; }
    .muted { color: #666; font-size: 12px; }
    .actions { display: flex; gap: 8px; margin-top: 16px; }
    button { padding: 10px 16px; border-radius: 10px; border: 0; cursor: pointer; }
    button.primary { background: #0a7; color: #fff; }
    pre {
      background: rgba(0,0,0,.05); padding: 12px; border-radius: 8px; overflow: auto; max-height: 300px;
    }
    .ok { color: #0a7; }
    .err { color: #d33; }
  </style>
</head>
<body>
  <h1>Web Push 送信</h1>
  <p class="muted">このページと <code>send.php</code> は <strong>/admin</strong> といった保護ディレクトリ内に置き、Basic認証やIP制限をかけてください。</p>

  <form id="form">
    <fieldset>
      <legend>通知内容</legend>
      <label for="title">タイトル</label>
      <input id="title" name="title" type="text" placeholder="お知らせ" required />

      <label for="body">本文</label>
      <textarea id="body" name="body" placeholder="本文を入力"></textarea>

      <label for="url">クリック先URL</label>
      <input id="url" name="url" type="url" placeholder="https://your-domain/" />
    </fieldset>

    <div class="actions">
      <button class="primary" type="submit">送信</button>
      <button type="reset">リセット</button>
    </div>
  </form>

  <h2>レスポンス</h2>
  <pre id="out" aria-live="polite"></pre>

  <script>
    const $ = (sel) => document.querySelector(sel);
    const out = $('#out');

    $('#form').addEventListener('submit', async (e) => {
      e.preventDefault();
      out.textContent = '送信中…';

      const title = $('#title').value.trim() || 'お知らせ';
      const body  = $('#body').value.trim();
      const url   = $('#url').value.trim() || '/';
      const payload = { title, body, url };

      try {
        const res = await fetch('./send.php', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          credentials: 'include' // 同一ディレクトリ保護のBasic認証を継承
        });

        const text = await res.text();
        let data;
        try { data = JSON.parse(text); } catch { data = { raw: text }; }

        if (!res.ok) {
          out.innerHTML = '<span class="err">HTTP ' + res.status + '</span>\n' + JSON.stringify(data, null, 2);
          return;
        }
        const summary = (data.ok ? '<span class="ok">OK</span>' : '<span class="err">NG</span>');
        out.innerHTML = summary + '\n' + JSON.stringify(data, null, 2);
      } catch (err) {
        out.innerHTML = '<span class="err">送信失敗: ' + (err && err.message || err) + '</span>';
      }
    });
  </script>
</body>
</html>

通知送信処理(public_html/admin/send.php)

<?php
// public_html/admin/send.php

// === 返却はJSON =====================================================
header('Content-Type: application/json; charset=utf-8');

require __DIR__ . '/../../vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

$input = json_decode(file_get_contents('php://input'), true) ?: [];
$title = (string)($input['title'] ?? 'お知らせ');
$body  = (string)($input['body']  ?? '');
$url   = (string)($input['url']   ?? '/');

// === DB 接続(調整してください)=====================================
$dsn  = 'mysql:host=localhost;dbname=push_demo;charset=utf8mb4';
$user = 'user';
$pass = 'password';

try {
  $pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  ]);
} catch (Throwable $e) {
  http_response_code(500);
  echo json_encode(['error' => 'DB connect failed', 'detail' => $e->getMessage()]);
  exit;
}

// === 送信対象の取得 ==================================================
$rows = $pdo->query('SELECT * FROM push_subscriptions')->fetchAll();
if (!$rows) {
  echo json_encode(['ok' => true, 'sent_ok' => 0, 'expired_removed' => 0, 'errors' => [], 'note' => 'no recipients']);
  exit;
}

// === WebPush 準備 ====================================================
$webPush = new WebPush([
  'VAPID' => [
    'subject'    => 'mailto:admin@example.com',  // 連絡先(適宜変更)
    'publicKey' => trim(file_get_contents(__DIR__ . '/../../keys/vapid_public.key')),
    'privateKey' => trim(file_get_contents(__DIR__ . '/../../keys/vapid_private.key')),
  ]
]);

$payload = json_encode([
  'title' => $title,
  'body'  => $body,
  'data'  => ['url' => $url],
], JSON_UNESCAPED_UNICODE);

// === キュー投入(まとめて送って flush) =============================
foreach ($rows as $r) {
  $sub = Subscription::create([
    'endpoint' => $r['endpoint'],
    'keys'     => ['p256dh' => $r['p256dh'], 'auth' => $r['auth']],
  ]);
  // TTLは任意(秒)。ここでは10分
  $webPush->queueNotification($sub, $payload, ['TTL' => 600]);
}

// === 送信&結果集計 ==================================================
$ok = 0; $expired = 0; $errors = [];
foreach ($webPush->flush() as $report) {
  $endpoint = (string)$report->getRequest()->getUri();

  if ($report->isSuccess()) {
    $ok++;
  } elseif ($report->isSubscriptionExpired()) {
    // 失効購読は削除
    $st = $pdo->prepare('DELETE FROM push_subscriptions WHERE endpoint = :e');
    $st->execute([':e' => $endpoint]);

    $expired++;
  } else {
    $errors[] = ['endpoint' => $endpoint, 'reason' => $report->getReason()];
  }
}

// === レスポンス ======================================================
echo json_encode([
  'ok' => true,
  'sent_ok' => $ok,
  'expired_removed' => $expired,
  'errors' => $errors,
]);

これで管理画面から入力した内容をすぐに購読者へ配信できるようになります。


運用とメンテナンスについて

実装後は、配信品質を保つための運用設計が重要です。本記事では、方針レベルで押さえるべきポイントのみを整理します。

  • 無効購読の整理方針:配信レポートで恒久的エラー(例:404/410)を検知した購読は定期的に削除し、DB肥大化を防ぐ。
  • 再購読・解除の導線:起動時に購読状態を確認し、未購読なら再購読ボタンを提示。ユーザーが自発的に解除できるUIも用意する。
  • 送信ポリシーと負荷管理:TTLやUrgencyを用いた優先度設計、送信間隔・バッチ分割・レート制御でスパイクを回避。
  • ログとモニタリング:送信日時・HTTPステータス・失敗理由を記録し、到達率・解除率などのKPIを継続監視。
  • VAPID鍵の運用:鍵の頻繁なローテーションは避け、やむを得ない変更時は再購読の移行計画を事前に設ける。
  • セキュリティとプライバシー:購読情報の取り扱いポリシー(保存期間・バックアップ・アクセス権限)を文書化し遵守する。

まとめ

本記事では、Service Worker と PHP(Minishlink/web-push)を使って Web Push 通知を実装する手順を、最小限の構成でわかりやすく解説しました。
ライブラリの導入から VAPID 鍵の生成、購読情報を管理するデータベースの設計、フロントエンドでの購読処理、そして管理画面からの通知送信まで、一通りの流れを構築することで、Web Push の仕組みと実装の全体像をご理解いただけたかと思います。

ただし、実運用を前提とする場合は、セキュリティやアクセス制御に十分な配慮が必要です。特に管理画面については、Basic認証や IP 制限などを適切に設定し、悪用を防ぐ対策を講じましょう。

また、対応環境にも注意が必要です。2025年8月現在、iPhone(Safari)では Web Push がサポートされるのはPWA としてホーム画面に追加された場合に限られています。Android や PC ブラウザと比べて機能や仕様に制限がありますので、ユーザー体験の違いを理解したうえで導入を検討することが重要です。

今回の内容はあくまで導入と動作確認を目的とした基本実装ですが、Web Push の活用を検討するうえでの出発点として、ぜひお役立てください。

Contact

ウェブサイトの制作や運用に関わる
お悩みやご相談
お気軽にお問い合わせ下さい

ウェブサイトと一口に言っても、企業サイトやECサイト、ブログ、SNSなど、その“カタチ”は目的に応じてさまざまであり、構築方法や使用する技術も大きく異なります。株式会社コナックスでは、お客様のご要望やブランドの個性を丁寧に汲み取り、最適なウェブサイトの“カタチ”をご提案いたします。

デザイン、ユーザビリティ、SEO対策はもちろん、コンテンツ制作やマーケティング戦略に至るまで、あらゆるフェーズでお客様のビジネスに寄り添い、成果につながるウェブサイトづくりをサポートいたします。私たちは、ウェブサイトの公開をゴールではなくスタートと捉え、お客様のビジネスの成功に向けて共に伴走してまいります。