Next.js Web Push Notifications: A Complete Guide with Code Examples
12 mins read
6 Likes
521 Views
Introduction
This guide will walk you through implementing rich push notifications in Next.js using the Web Push API. We'll cover the entire notification payload structure:
type NotificationPayload = { title: string; message: string; userId?: string; subscription?: any | any[]; image?: string | null; icon?: string | null; badge?: string | null; tag?: string; timestamp?: number; vibrate?: boolean; renotify?: boolean; requireInteraction?: boolean; silent?: boolean; actions?: any[]; url?: string | null; data?: any; ttl?: number; urgency?: string; topic?: string | null; };
Prerequisites
1. Install Required Packages
npm install web-push next-pwa
These packages provide:
- web-push: Server-side library for sending push notifications
- next-pwa: PWA support for Next.js
Project Setup
1. Configure next.config.js
const withPWA = require('next-pwa')({ dest: 'public', register: true, skipWaiting: true, }); module.exports = withPWA({ // your existing Next.js config });
2. Generate VAPID Keys
Create a script to generate your VAPID keys:
// scripts/generate-vapid.js const webpush = require('web-push'); const vapidKeys = webpush.generateVAPIDKeys(); console.log('VAPID Keys:', vapidKeys);
Run the script and save the keys in your .env file:
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key VAPID_PRIVATE_KEY=your_private_key
Service Worker Implementation
Create public/sw.js:
// public/sw.js self.addEventListener('push', function(event) { const payload = event.data?.json() ?? {}; const options = { title: payload.title || 'New Notification', body: payload.message, icon: payload.icon || '/icon.png', image: payload.image, badge: payload.badge, tag: payload.tag, timestamp: payload.timestamp || Date.now(), vibrate: payload.vibrate ? [200, 100, 200] : undefined, renotify: payload.renotify || false, requireInteraction: payload.requireInteraction || false, silent: payload.silent || false, actions: payload.actions || [], data: payload.data || {}, }; event.waitUntil( self.registration.showNotification(options.title, options) ); }); self.addEventListener('notificationclick', function(event) { event.notification.close(); if (event.notification.data.url) { event.waitUntil( clients.openWindow(event.notification.data.url) ); } });
Backend Implementation
Create an API route to handle subscriptions and sending notifications:
// pages/api/push/subscribe.ts import { NextApiRequest, NextApiResponse } from 'next'; import webpush from 'web-push'; webpush.setVapidDetails( 'mailto:your-email@example.com', process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ); export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { const subscription = req.body.subscription; const payload: NotificationPayload = req.body.payload; await webpush.sendNotification( subscription, JSON.stringify(payload), { TTL: payload.ttl || 24 * 60 * 60, urgency: payload.urgency || 'normal', topic: payload.topic || undefined, } ); res.status(200).json({ success: true }); } catch (error) { console.error('Push notification error:', error); res.status(500).json({ error: 'Failed to send notification' }); } }
Frontend Implementation
1. Create Push Notification Service
// services/pushNotification.ts export class PushNotificationService { private static instance: PushNotificationService; private swRegistration: ServiceWorkerRegistration | null = null; private constructor() {} static getInstance(): PushNotificationService { if (!this.instance) { this.instance = new PushNotificationService(); } return this.instance; } async initialize(): Promise { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { throw new Error('Push notifications not supported'); } try { this.swRegistration = await navigator.serviceWorker.register('/sw.js'); } catch (error) { console.error('Service Worker registration failed:', error); throw error; } } async requestPermission(): Promise { return await Notification.requestPermission(); } async subscribeToPushNotifications(): Promise { try { const permission = await this.requestPermission(); if (permission !== 'granted') { throw new Error('Permission not granted'); } const subscription = await this.swRegistration?.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY }); return subscription || null; } catch (error) { console.error('Failed to subscribe:', error); return null; } } async sendNotification(payload: NotificationPayload): Promise { try { const subscription = await this.subscribeToPushNotifications(); if (!subscription) { throw new Error('No push subscription'); } const response = await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ subscription, payload, }), }); return response.ok; } catch (error) { console.error('Send notification error:', error); return false; } } }
2. Create React Component
// components/PushNotificationButton.tsx import { useState, useEffect } from 'react'; import { PushNotificationService } from '../services/pushNotification'; export default function PushNotificationButton() { const [isSubscribed, setIsSubscribed] = useState(false); const [pushService, setPushService] = useState(null); useEffect(() => { const service = PushNotificationService.getInstance(); service.initialize().then(() => { setPushService(service); }); }, []); const handleSubscribe = async () => { if (!pushService) return; try { const subscription = await pushService.subscribeToPushNotifications(); setIsSubscribed(!!subscription); if (subscription) { await pushService.sendNotification({ title: 'Welcome!', message: 'You have successfully subscribed to notifications', icon: '/icon.png', image: '/welcome-image.jpg', badge: '/badge.png', tag: 'welcome', vibrate: true, requireInteraction: true, actions: [ { action: 'explore', title: 'Explore App', icon: '/explore-icon.png' } ], url: '/dashboard', data: { userId: 'user123' } }); } } catch (error) { console.error('Subscription failed:', error); } }; return ( {isSubscribed ? 'Notifications Enabled' : 'Enable Notifications'} ); }
Testing
Browser Support Check
if ('Notification' in window && 'serviceWorker' in navigator && 'PushManager' in window) { // Push notifications are supported } else { // Push notifications are not supported }
Debugging Tips
- Use Chrome DevTools > Application > Service Workers to monitor registration
- Check Console for subscription and notification errors
- Verify VAPID keys are correctly set in environment variables
- Test with different payload configurations
Best Practices
- Always request notification permission after user interaction
- Keep payload size under 4KB
- Implement proper error handling
- Use appropriate TTL values based on content urgency
- Provide clear opt-out mechanisms
- Monitor notification delivery rates
- Implement proper subscription cleanup
Share: