Next.js Web Push Notifications: A Complete Guide with Code Examples

Next.js Web Push Notifications: A Complete Guide with Code Examples
Photo by jamie452 on Unsplash
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

  1. Use Chrome DevTools > Application > Service Workers to monitor registration
  2. Check Console for subscription and notification errors
  3. Verify VAPID keys are correctly set in environment variables
  4. 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:

Comments

0

Join the conversation

Sign in to share your thoughts and connect with other readers

No comments yet

Be the first to share your thoughts!