Next.js Web Push Notifications: A Complete Guide with Code Examples
12 mins read
6 Likes
535 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: