firebaseからNext.jsのに向けてWebPushを送ってみる

# はじめに

こんにちは、エンジニアのkeitaMaxです。

firebaseからNext.jsのプロジェクトに向けてPushを送ってみようと思います。

https://qiita.com/yu-ka/items/5f1b670d2f51a63f29a0

基本的にこの記事を元に行っていこうと思います

# firebaseの設定

まうfirebaseに新しいプロジェクトを作成してAPIKeyなどを取得するようにします。

プロジェクトの作成

![スクリーンショット 2025-06-14 10.11.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/2e093105-c29e-4fcf-93a9-442f745ab553.png)


アプリの登録

![スクリーンショット 2025-06-14 10.11.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/7765b2ac-4a3b-44fe-8156-46d9a2e37703.png)


![スクリーンショット 2025-06-14 10.12.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/f91ce4c2-1067-4f89-9ba3-800136d53aff.png)

この画面でapiKeyなどが出てくるのでメモっておきましょう


Keyの作成

プロジェクトの設定＞Cloud Messaging＞Generate key pair

![スクリーンショット 2025-06-14 10.17.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/53981c76-5043-4a71-993c-ca7accfa22ee.png)

![スクリーンショット 2025-06-14 10.18.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/1107229f-8075-4e0c-a9b4-2749068ffc65.png)


# インストール

以下でfirebaseのライブラリをインストールします。

```
npm install firebase
```

# コード

基本的には参考元と同様にしますが、同じだとESLintでエラーがでていたので、少し修正しています。

```ts:src/firebase/config.ts
"use client"

import { initializeApp } from 'firebase/app'
import { getMessaging } from 'firebase/messaging'

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
}

const app = initializeApp(firebaseConfig)

let messaging: ReturnType<typeof getMessaging> | null = null

if (typeof window !== 'undefined') {
  try {
    messaging = getMessaging(app)
  } catch (error) {
    console.error('Firebase Messaging初期化エラー:', error)
  }
}

export { messaging }
```


```ts:src/firebase/useFCM.ts
"use client"

import { useEffect, useState } from "react"
import { MessagePayload, onMessage } from "firebase/messaging"
import { messaging } from "~/firebase/config"
import useFCMToken from "./useFCMToken"

export const useFCM = () => {
  const fcmToken = useFCMToken()
  const [messages, setMessages] = useState<MessagePayload[]>([])

  useEffect(() => {
    if (typeof window === 'undefined') return
    if (!("serviceWorker" in navigator)) return
    if (!messaging) return

    try {
      const unsubscribe = onMessage(messaging, (payload) => {
        setMessages((messages) => [...messages, payload])
      })

      return () => unsubscribe()
    } catch (error) {
      console.error('FCM初期化エラー:', error)
    }
  }, [fcmToken])

  return { fcmToken, messages }
}

```

```ts:src/firebase/useFCMToken.ts
"use client"

import { useEffect, useState } from "react"
import { getToken, isSupported } from "firebase/messaging"
import { messaging } from "~/firebase/config"
import useNotificationPermissionStatus from "./useNotificationPermissionStatus"

const useFCMToken = () => {
  const permission = useNotificationPermissionStatus()
  const [fcmToken, setFcmToken] = useState<string | null>(null)

  useEffect(() => {
    const retrieveToken = async () => {
      if (typeof window === "undefined") return
      if (!("serviceWorker" in navigator)) return
      if (permission !== "granted") return
      if (!messaging) return

      try {
        const isFCMSupported = await isSupported()
        if (!isFCMSupported) return

        // Service Workerの登録
        await navigator.serviceWorker.register('/web-push/firebase-messaging-sw.js', {
          scope: '/web-push/'
        })

        const token = await getToken(messaging, {
          vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
          serviceWorkerRegistration: await navigator.serviceWorker.getRegistration()
        })
        setFcmToken(token)
      } catch (error) {
        console.error('FCMトークン取得エラー:', error)
      }
    }

    retrieveToken()
  }, [permission])

  return fcmToken
}

export default useFCMToken

```

```ts:src/firebase/useNotificationPermissionStatus.ts
"use client"

import { useEffect, useState } from "react"

type NotificationPermission = "default" | "granted" | "denied"
// type PermissionName = "notifications"

const useNotificationPermissionStatus = () => {
  const [permission, setPermission] = useState<NotificationPermission>("default")

  useEffect(() => {
    if (typeof window === "undefined") return
    if (!("Notification" in window)) return

    const handler = () => setPermission(Notification.permission)
    handler()

    try {
      Notification.requestPermission().then(handler)
      navigator.permissions
        .query({ name: "notifications" })
        .then((notificationPerm) => {
          notificationPerm.onchange = handler
        })
        .catch((error) => {
          console.error('通知許可状態の取得エラー:', error)
        })
    } catch (error) {
      console.error('通知許可のリクエストエラー:', error)
    }
  }, [])

  return permission
}

export default useNotificationPermissionStatus

```

```env:.env
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
NEXT_PUBLIC_FIREBASE_VAPID_KEY=

```

```js:firebase-messaging-sw.js
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/8.8.0/firebase-app.js")
// eslint-disable-next-line no-undef
importScripts("https://www.gstatic.com/firebasejs/8.8.0/firebase-messaging.js")

// eslint-disable-next-line no-undef
const firebaseConfig = {
  apiKey: "AIzaSyBblSP-",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
}

// eslint-disable-next-line no-undef
firebase.initializeApp(firebaseConfig)
// eslint-disable-next-line no-undef
const messaging = firebase.messaging()

messaging.onBackgroundMessage((payload) => {
  console.log(
    "[firebase-messaging-sw.js] Received background message ",
    payload
  )
  const notificationTitle = payload.notification.title
  const notificationOptions = {
    body: payload.notification.body,
    icon: "/web-push/logo.png",
  }
  self.registration.showNotification(notificationTitle, notificationOptions)
})

```

`firebase-messaging-sw.js`はGitには上げていません。

以下のようにGitHubActionsでbuildする時に作成するようにしました。
```yml:deploy.yml
name: Deploy to GitHub Pages

# on: [pull_request]
on:
  push:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: 'npm'
      - name: Copy .env.example
        run: cp .env.example .env
      - name: Inject Firebase secrets into .env
        run: |
          echo "NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}" >> .env
          echo "NEXT_PUBLIC_FIREBASE_VAPID_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_VAPID_KEY }}" >> .env
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Setup Pages
        uses: actions/configure-pages@v4
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./out

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
```




# テスト

実際にテストでPushを送ってみましょう。

`npm run dev`をしてNext.jsのアプリケーションを起動させておきます


firebase上でPushを作成します。

![スクリーンショット 2025-06-14 10.24.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/5ff6c6b9-5ff1-4bad-9053-e0bd6bc872a0.png)

![スクリーンショット 2025-06-14 10.26.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/27a206c0-b11c-4425-9817-4f897610024f.png)

これでメッセージの準備はできました。

次にNextjs側のトークンを取得します。
npm run dev すると出てくるトークンをコピーして入力します。

![スクリーンショット 2025-06-14 10.26.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/71831d80-4364-4ae2-a696-b01b7178ea2f.png)


これをfirebaseの方に入力します。

![スクリーンショット 2025-06-14 10.26.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/48aac955-d4c2-4f12-af24-58b2b2569cb5.png)

この状態でテストとすると、Push通知が送られてきて

![スクリーンショット 2025-06-14 10.27.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715123/b1594723-e03b-4ee2-9af2-06d703422a65.png)

このような感じでメッセージが表示されます

# おわりに

この記事での質問や、間違っている、もっといい方法があるといったご意見などありましたらご指摘していただけると幸いです。

最後まで読んでいただきありがとうございました！

# 参考

https://qiita.com/yu-ka/items/5f1b670d2f51a63f29a0

https://firebase.google.com/codelabs/firebase-nextjs?hl=ja#1