FirebaseコンソールからWebプッシュ通知を配信する(実装編)

この記事は「機能紹介編」の続きになります。前回はFirebase Cloud Messaging(FCM)コンソールから Webプッシュ通知を配信する流れをご紹介しました。
今回はその続編として、trogで実運用中の仕組みを新規のNext.js 15に実装することで、具体的な手順を説明していきます。
この実装では、FirebaseコンソールだけでWebプッシュを配信できるように設計しました。
配信のたびにターミナルからコマンドを叩くことも、PostmanのようなAPIツールも不要です。広報担当の方にも配信運用できる構成を、Next.js15の最小実装でご紹介します🙋♀️
前提条件
ゴールと想定読者
この記事では、Firebaseコンソールページの「キャンペーン」ページから Web Push 通知をPCへ配信し、通知クリックで任意パスに遷移できる最小実装を作ることがゴールになります。
Web Push 通知機能を実装してみたいエンジニアに向けて書きました。
この領域が専門外のエンジニアの方や、Webアプリケーション製作が初心者の方にも、雰囲気はつかんでいただけるように、なるべく噛み砕いて説明します。
使うもの
- Next.js 15(App Router / TypeScript)
- Chrome ブラウザ(最新版)
- Firebaseコンソール(テスト送信用UI)
- 2025年9月時点では無料で利用できます。
この記事の実装・検証はすべてWindows PCで実施しています。OSの制約はとくにありません、macでも動かせるはずです。
本記事で取り扱わない機能
- ソース内の操作をトリガーとするサーバ送信(Firebase SDK for Cloud Functions, Firebase Admin SDK)
- iPhone へのプッシュ通知(PWA化)
将来的に別の記事として投稿するかもしれません。(公開時期未定)
全体像とデータの流れ
この実装では、ブラウザ(フォアグラウンド)、Service Worker(バックグラウンド)、Firebaseコンソールの3つで役割分担します。
フォアグラウンド(ブラウザ)
通知の許可をユーザーに求め、getToken()
で 登録トークンを取得します。ページ表示中に届いたメッセージは onMessage()
で受け取り、必要なら画面内で案内を出します。
Service Worker
ブラウザのタブが閉じている状態、もしくは非アクティブでもメッセージを受け取り、通知を表示します。
さらに、通知クリック時の遷移を担当します。本記事では data.path に指定したパスへ遷移させる設計にしています(例:/blog/xxxx
)。
Firebaseコンソール
送信UIから、取得した 登録トークン宛にテスト送信します。タイトル・本文に加えて、データペイロードに path を指定します。(key: path
, value: /blog/xxxx
)
data.path について
data.path には「Push通知で読者に届けたいページ(更新記事など)」の相対パスを入れる想定で実装しています。未指定の場合はルート(/
)にフォールバックします。
通知の内部的な流れ
1) 通知許可 → トークン取得
ユーザーが通知を許可すると、ブラウザで getToken(vapidKey, { serviceWorkerRegistration })
を実行し、端末固有の登録トークンを取得します。
2) Firebaseコンソールで送信
コンソールの「キャンペーン」ページから、配信先(Webアプリの全ユーザー/トピック/セグメント)を選び、タイトル・本文(+必要なら画像)を入力。任意で data.path
に遷移先を設定して送信可能な実装にしています。
端末トークンの収集やAPI呼び出しを運用側で持つ必要はありません。トークンの発行・更新はクライアント(Firebase SDK)が行い、配信対象はFCM側で解決されるため、広報担当の方にもUIだけで運用できます。
3) 受信 → クリックで遷移
端末がフォアグラウンドなら onMessage()
、バックグラウンドなら Service Worker が受信し、通知を表示します。
ユーザーが通知をクリックすると、Service Worker がdata.pathの値を読んでそのパスへ遷移させます。自然な形で更新記事へと誘導できます。
Service Worker のパスとスコープ
本記事のデモは public/console/firebase-messaging-sw.js
を scope: '/console/'
で登録します。他デモと衝突しないよう、/console/
配下に分離しました。Service Workerの登録URLと scope
を揃えることで、今後投稿するかもしれない他のデモと干渉せずに並存できるようにしてあります。
FCMから設定情報を取得
ここでは 「Web アプリを登録して Firebase Config を取得」→「VAPID 公開鍵を生成」 の2点だけに絞って案内します。どちらも Firebaseコンソール から行えます。
1) Web アプリを追加して Firebase Config を取得
- Firebaseコンソールで対象プロジェクトを開き、Web アイコン(
</>
←こんなの)からアプリを登録します。 - 画面の手順に沿って進めると、Firebase Config(apiKey / authDomain / projectId …) が表示されます。これを Next.js 側の環境変数に記載してください。
2) Web Push の VAPID 公開鍵を生成
- コンソールの [設定]→[Cloud Messaging] を開き、ページ下部の [ウェブの構成 → ウェブプッシュ証明書] セクションへ移動します。
- [Generate key pair] をクリックして鍵ペアを生成します。表示された公開鍵を、クライアントの
getToken()
に渡すvapidKey
として使用します。
(参照:Firebase を JavaScript プロジェクトに追加する - firebase , Firebase Cloud Messaging を使ってみる - firebase )
アプリの実装
Next.jsの準備
新規のNext.jsプロジェクトを用意します。Next.js公式サイトなどを参考に構築してください。
完了したら、Firebaseパッケージを導入します。
パッケージのバージョンは厳密に固定しておきます。このあと説明するService WorkerにインポートするCDNのバージョンと合わせることで、予期せぬ不具合を避けようという狙いです。
現時点(2025年9月)では、Firebase Cloud Messaging を使用してメッセージを受信する - firebase で紹介されているCDNのバージョンが10.13.2
になっているので、これに合わせます。
npm i firebase@10.13.2
以降の例では、デモを分けて並存できるよう /console
ルートを使います
- ページ:
/app/console/page.tsx
- Service Worker:
/public/console/firebase-messaging-sw.js
- Config:
/public/console/firebase-config.js
開発ではlocalhostのみ、Service Workerファイル(firebase-messaging-sw.js
)が登録可能です。
サービスワーカー(/public/console/firebase-messaging-sw.js
)
このファイルがFCM のバックグラウンド受信と通知クリック時の遷移を担当します。ブラウザのページ(フォアグラウンド)が閉じていても、ここが受信・表示・遷移を完結させます。
1) 通知クリックは最初にフック
self.addEventListener('notificationclick', function(event) {
event.notification.close();
const baseURL = self.location.origin;
const path = event.notification.data.FCM_MSG.data.path || '/';
try {
const url = new URL(path, baseURL).toString();
clients.openWindow(url);
} catch (e) {
console.error('Invalid URL:', url);
}
});
FCMのスクリプトを読む前に notificationclick
を登録しておかないと、既定挙動で上書きされる可能性があります。(参照:Firebase Cloud Messaging を使用してメッセージを受信する - Firebase)
2) 任意パスへ遷移
通知の移動先をその他のオプションのカスタムデータ(path
)で指定します。通知配信のたびに「どのページへ誘導するか」をFirebaseコンソール側で差し替えられるため、運用がシンプルになります。
例えば上記のキャンペーンページで、キーにpath
、値を/blog/2025/awesome-post
と指定した通知を配信することで、通知クリックで該当記事へと遷移します。なお、未指定時は / にフォールバックします。
この方針にすることで、運用担当者は data.path
に「宣伝したい更新記事のパス」を入れるだけで、「通知クリック → サイト内遷移」という動作が実現できます。
⚠️外部URLへの遷移は禁止
data.path
に絶対URLを入れるよう実装を変更すれば、技術的には外部サイトへも遷移できますが、本記事ではセキュリティ上の理由から外部サイトへの遷移は不可能にすべきだと主張しておきます。
外部URLへの遷移は、通知は受け手に「公式の連絡」として映りやすく、そこから他のサイトへ飛ばせる設計はフィッシングの格好の入口になります。さらに、任意のURLに誘導できる仕組みはオープンリダイレクトと同質で、攻撃者に踏み台として悪用される余地も生まれます。加えて(今回の記事では扱いませんが)、PWAとしてホーム画面から起動している場合、外部サイトへ遷移するとスタンドアロン環境の外(Safari/Chromeの通常タブ)で開いてしまい、アプリ内の状態や体験が途切れてしまいます。
こうしたセキュリティとUXの観点から、本記事の実装では data.path
に指定するリンクを同一オリジンの相対パスに限定し、外部URLは受け付けない方針を採用します。これにより、通知クリックは常にアプリ内遷移で完結し、安全で一貫した利用体験を保てます。
3) FCM SDK を読み込み、初期化する
importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-compat.js');
importScripts('/console/firebase-config.js'); // 生成された設定を読み込む
firebase.initializeApp(self.firebaseConfig);
const messaging = firebase.messaging();
CDN のバージョン固定を(firebase-app-compat.js
/ firebase-messaging-compat.js
) 10.13.2 に固定しています。前述のとおり、Firebase Cloud Messaging を使用してメッセージを受信する - firebase で紹介され ているCDNのバージョンに合わせています。実装する際には、このページの最新のバージョンを採用してください。
設定ファイル/console/firebase-config.js
はビルド時に環境変数 .env*
から生成します。本番/ステージングなど、環境ごとに自動切替でき、手作業による設定ミスを防げます。(※Web用Configは公開前提の識別情報です。)
ほぼ全部出してますが、Service Workerの全体像は後日公開するGithubリポジトリを確認してください。(2025年10月中旬予定)
クライアント実装
この記事は FCM の「通知配信を確実に動かす設計」にフォーカスして解説します。Next.js(App Router)としての一般的な組み立てや画面構成に踏み込みすぎると主題から逸れるため、本章では落とし穴になりやすい要点のみ取り上げます。
1) 迷惑通知にしないための許可設計
通知の許可は、必ずユーザーの操作から始めるのが原則です。各ブラウザのポリシーで、ユーザーの明示的なジェスチャー(クリックなど)を伴わない許可リクエストは制限されています。勝手に通知許可を要求するダイアログを出す実装は、ユーザーにとって迷惑であり、サイトの信頼を損なうだけです。
そこで本実装では、ボタンを押してもらったタイミングでだけNotification.requestPermission()
を呼び、許可された場合にだけ続けて fetchToken()
を実行します。
const requestAndGetToken = async () => {
const p = await Notification.requestPermission();
setPermission(p);
if (p === 'granted') await fetchToken();
};
なおデモでは動作確認のためにトークンを表示していますが、本来は画面に出す必要はまったくありません。許可済みの端末ではページ表示後に自動で getToken()
を実行し、その結果をサーバへ登録します。ユーザーには「通知が有効」などの簡潔な表示だけを行い、通知の再許可・解除はブラウザ設定やヘルプへの導線で案内します。
2) SSR安全化:isSupported()
と Notification.permission
の扱い
まず、Next.js(App Router)ではサーバー側で最初の描画が行われるため、SSR中は Notification
が存在しません。ここで安易に Notification.permission
を読みにいくと、ビルド/実行時に落ちたり、クライアントに切り替わった瞬間に状態が変わって**許可ボタンが一瞬だけ表示される“チラ見え”**が起きます。そこで初期状態はあくまで安全側(permission = 'default'
/ supported = null
など)に置き、マウント後にだけ isSupported()
と Notification.permission
を評価して、実際のブラウザ状態へ同期させます。
const [supported, setSupported] = useState<boolean | null>(null);
const [permission, setPermission] = useState<NotificationPermission>('default');
const [isPermissionKnown, setIsPermissionKnown] = useState(false);
useEffect(() => {
(async () => {
setSupported(await isSupported().catch(() => false));
if (typeof Notification !== 'undefined') setPermission(Notification.permission);
setIsPermissionKnown(true); // ← 判定完了後にUIを出す
})();
}, []);
UIもそれに合わせて、権限の判定が終わるまでは許可ボタンを描画しないのがポイントです。例えば isPermissionKnown
というフラグを用意し、初回マウント時に Notification.permission
を取得したタイミングで true
に切り替えます。画面側はisPermissionKnown && permission !== 'granted'
のときだけボタンを出すようにすれば、SSR→CSRの移行時にボタンがチラッと出る問題を確実に防げます。
3) navigator.serviceWorker.ready
を待ってから getToken()
Service Worker は登録した直後だと、まだページを制御(controlling)していないことがあります。この状態で getToken()
に登録直後の registration
を渡すと、内部で参照される registration.pushManager
が未定義のままになり、例の TypeError で落ちることがあるんですね。そこで、navigator.serviceWorker.ready
を唯一の情報源として使い、ページの制御が確定してから ServiceWorkerRegistration
を受け取り、その値を getToken()
に渡す、という流れに固定します。
実装はシンプルで、トークン取得の入口を次のようにします。ready
の解決を待ってから vapidKey
とセットで getToken()
を呼ぶだけです。
const fetchToken = async () => {
const reg = await navigator.serviceWorker.ready; // ← controlling まで待つ
const messaging = getMessaging(firebaseApp);
const vapidKey = process.env.NEXT_PUBLIC_FIREBASE_WEBPUSH_KEY!;
return await getToken(messaging, { vapidKey, serviceWorkerRegistration: reg });
};
あわせて、ユーザーの連打による二重実行を避けるため、isFetchingToken
と token
でガードしておくと安心です。これで「準備前に呼んで落ちる」だとか、「二重に呼んで競合する」といった不安定な動作を防いでいます。
4) vapidKey
(公開鍵)は NEXT_PUBLIC_
の環境変数から渡す
vapidKey
は Web Push 用の公開鍵です。クライアント側(ブラウザ)から参照する値なので、環境変数名に NEXT_PUBLIC_
を付けて公開前提で渡すのが正しい扱いになります。ここを通常のサーバ用変数にしてしまうとビルド時に埋め込まれず、getToken()
が失敗しがちです。
運用としては .env.local
に保存 → 開発サーバを再起動して反映、がシンプルで確実です。なお公開鍵なので秘匿情報ではありません。
# .env.local(一部)
NEXT_PUBLIC_FIREBASE_WEBPUSH_KEY=BP... # ← これが無いと getToken が失敗
// クライアントでの参照例
const vapidKey = process.env.NEXT_PUBLIC_FIREBASE_WEBPUSH_KEY!;
const t = await getToken(messaging, { vapidKey, serviceWorkerRegistration: reg });
許可していない第三者ドメインからの悪用を抑止するため、Google Cloud Console で対象となるAPI キーに「アプリケーションの制限」→「 HTTP リファラー(ウェブサイト)」を設定し、本番/ステージングのドメインだけ許可します。必要に応じてAPI の制限で利用 API も絞り込みます。(API キーを管理 - firebase)。併せて Firebase App Check(reCAPTCHA v3/Enterprise) を有効化すると、正規クライアント以外からのリクエストをさらにブロックできます。(Firebase App Check - Firebase)
5) firebase-config.js
はビルド/起動前に自動生成
firebase-config.js
は ビルド/起動のたびに自動生成する運用にします。手で置き換える方式だと、開発・ステージング・本番の切り替え漏れや、間違った設定をリポジトリにコミットしてしまう事故が起きやすいからです。環境固有の値は .env*
に集約し、生成物は /public/console/firebase-config.js
に書き出して Git 管理から外すのが安全です。
実装はシンプルで、predev
/ prebuild
でスクリプトを走らせるだけ。**必須キーが欠けていたら即座に失敗(Fail Fast)**させ、原因が曖昧なランタイムエラーを未然に防ぎます。
// scripts/generate-config.js(抜粋・必須キー検証例)
const required = ['apiKey','projectId','messagingSenderId','appId'];
const missing = required.filter(k => !cfg[k]);
if (missing.length) { console.error('Missing:', missing.join(',')); process.exit(1); }
この方式なら、環境の切り替えは .env
を差し替えるだけで済み、ビルド/起動前に常に正しい firebase-config.js
が生成されます。結果として、設定ミスに強く、レビューもしやすい構成になります。
動作チェック(FCM からテスト送信)
本番運用は必ず HTTPS が大前提です。ステージング/本番で確認する際は、https 環境の正しいドメイン上で検証してください。開発中のみ http://localhost
で Service Workerの登録が許可されています。
- 開発サーバを起動し、
http://localhost:3000/console
を開きます。画面に「Web Push を有効化」カードが表示されます。 - 「通知を有効化」をクリックするとブラウザの許可ダイアログが出るので、許可を選びます。 許可後、ページが自動で
getToken()
を実行し、登録トークンが取得されます(デモではテキストエリアに表示) - 次にFirebaseコンソールでキャンペーン(またはテストメッセージ)を作成します。右側の「デバイスのテスト」で、宛先に先ほどの登録トークンを貼り付けます。メッセージの設定では、タイトルと本文を入力し、追加オプション → カスタムデータに
path: console
(または遷移させたい相対パス)を指定します。
送信すると、ブラウザに通知が届きます。通知をクリックしたら、Service Worker が data.path
を読み取ります。指定したパスに遷移すれば成功です。(console
なんかがおすすめです。)
ページが開いている最中はフォアグラウンドで受信(onMessage
画像参照)、閉じている/非アクティブのときは Service Worker がバックグラウンド受信して通知を表示します。
本番の通知配信の方法は機能紹介編で紹介しております。まだ見てないよーという方は、よければご一読ください🙋♀️
つまずきポイント
「Installations: Missing App configuration value: "projectId"
」のようなエラーは、環境変数の不足やキー名の誤りが原因で起きることがほとんどです。
このリポジトリには Firebase/FCM の設定値を同梱していません。各自で FCM プロジェクトを作成し、.env.local
に値を用意してください。NEXT_PUBLIC_
の付け忘れにも注意してください。
確認手順
.env.local
を再確認- 必須:
NEXT_PUBLIC_FIREBASE_API_KEY / AUTH_DOMAIN / PROJECT_ID / STORAGE_BUCKET / MESSAGING_SENDER_ID / APP_ID
- Web Push:
NEXT_PUBLIC_FIREBASE_WEBPUSH_KEY
(VAPID 公開鍵) - すべて
NEXT_PUBLIC_
プレフィックス付きであること。
- 必須:
- 生成物の整合性
- SW が読む
/public/console/firebase-config.js
に、.env.local
の値が正しく出力されているか確認(生成スクリプト推奨)。
- SW が読む
- 開発サーバを再起動
.env
を更新したらnpm run dev
を必ず再起動。古い値のまま動いているケースが多いです。
この3点を満たしていれば、projectId
などの不足エラーは解消されるはずです。
よくありそうな質問
Q. デモサイトの公開予定はあるか?
現時点では予定していません。
ただし、trog のプッシュ通知で同等の体験を試せます。今回の記事は「Firebaseコンソールからの配信」ですが、trog では Firebase Functions / Admin SDK 経由の配信を採用しており、テスト通知ボタンからすぐに端末へ通知を送れます。
- 登録すると、その場でテスト通知を受け取れます。
- 登録を維持すると、trog の更新通知が不定期に届きます(いつでも解除可能)。
また年内を目処に、本記事で紹介した方針に沿った、よりモダンで使いやすい通知画面へ順次アップデート予定です。
Q. 実装の全体像が見たい
Githubリポジトリをご用意しましたが、2025年10月中旬公開予定です。
少しだけお待ちください。
microayatron/fcm-webpush-next15 - Github
おわりに
Webのプッシュ通知はとても強力な機能でありながら、まだ一般的とは言えません。
便利さと同じだけリスクもあり、設計を誤れば迷惑な体験に直結します。ネイティブアプリより自由度が高いぶん、許可要求の出し方・遷移先・頻度まで丁寧に設計する「責任」があります。
本記事は、Firebaseコンソール運用を前提に、安全でわかりやすい最小実装をまとめました。小さく試し、ユーザーの反応を見ながら改善する。その積み重ねが、通知体験とブランドへの信頼を育てます。
誰かの操作体験を「邪魔する」のではなく、有益な情報を届けられるような「役立つ」通知を目指していきましょう。
- この記事を共有する