Table of Contents#
- Prerequisites
- Understanding the Push Notifications Flow
- Setting Up the Project
- Implementing the Service Worker
- Sending Push Notifications with URL Data
- Testing the Implementation
- Troubleshooting Common Issues
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have the following:
- Basic Knowledge: Familiarity with HTML, JavaScript, and Node.js (for the backend).
- HTTPS Environment: Push notifications require HTTPS (even for local development; use
localhostwith a self-signed certificate or tools like mkcert for trusted local HTTPS). - Service Worker Foundation: Understanding of service workers (they run in the background and handle events like
pushandnotificationclick). - Tools:
- Chrome browser (for testing, as we’ll focus on Chrome-specific behavior).
- Node.js and npm (to run the backend server).
- A code editor (e.g., VS Code).
Understanding the Push Notifications Flow#
To implement clickable notifications that open a URL, let’s first break down the end-to-end flow:
- User Grants Permission: The user allows your site to send notifications.
- Subscription: The client (browser) generates a push subscription object (containing an endpoint URL provided by the browser’s push service).
- Store Subscription: Send this subscription to your backend server and store it (e.g., in a database).
- Server Sends Push Message: When you want to send a notification, your server sends a push message to the browser’s push service (using the subscription endpoint).
- Push Service Delivers Message: The browser’s push service forwards the message to the client’s service worker.
- Service Worker Shows Notification: The service worker listens for the
pushevent, then displays a notification. - Handle Click: When the user clicks the notification, the service worker listens for the
notificationclickevent and opens the specified URL.
Setting Up the Project#
Let’s set up a minimal project with a client (frontend) and server (backend).
Step 1: Project Structure#
chrome-push-notifications/
├── client/
│ ├── index.html
│ ├── service-worker.js
│ └── client.js
├── server/
│ ├── server.js
│ └── package.json
└── .env (for server environment variables)
Step 2: Frontend Setup (Client)#
index.html#
This is the main page where users grant permission and subscribe to push notifications.
<!DOCTYPE html>
<html>
<head>
<title>Chrome Push Notifications Demo</title>
</head>
<body>
<h1>Push Notifications Demo</h1>
<button id="subscribeBtn">Subscribe to Notifications</button>
<script src="client.js"></script>
</body>
</html> client.js#
Handles permission requests, service worker registration, and push subscription.
// Register service worker on page load
if ('serviceWorker' in navigator && 'PushManager' in window) {
window.addEventListener('load', async () => {
try {
// Register service worker
const registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('ServiceWorker registered:', registration.scope);
// Set up subscribe button click handler
document.getElementById('subscribeBtn').addEventListener('click', () =>
subscribeUserToPush(registration)
);
} catch (error) {
console.error('ServiceWorker registration failed:', error);
}
});
}
// Request user permission and subscribe to push
async function subscribeUserToPush(registration) {
try {
// Request notification permission
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied');
}
// Subscribe to push notifications
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // Notification must be visible to the user
applicationServerKey: urlBase64ToUint8Array(
'YOUR_VAPID_PUBLIC_KEY' // Replace with your VAPID public key
)
});
// Send subscription to backend server (to store)
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
console.log('User subscribed to push:', subscription);
} catch (error) {
console.error('Failed to subscribe to push:', error);
}
}
// Helper: Convert VAPID public key from base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
} Step 3: Backend Setup (Server)#
We’ll use Node.js with Express and the web-push library to send push notifications.
server/package.json#
{
"name": "push-server",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2",
"web-push": "^3.5.0",
"dotenv": "^16.3.1"
}
} server/server.js#
This server handles storing subscriptions and sending push notifications with a custom URL.
require('dotenv').config();
const express = require('express');
const webpush = require('web-push');
const app = express();
app.use(express.json());
// Configure VAPID keys (generate once and store in .env)
const vapidKeys = {
publicKey: process.env.VAPID_PUBLIC_KEY,
privateKey: process.env.VAPID_PRIVATE_KEY
};
webpush.setVapidDetails(
'mailto:[email protected]', // Contact email (required by push services)
vapidKeys.publicKey,
vapidKeys.privateKey
);
// Store subscriptions (in-memory for demo; use a database in production)
let subscriptions = [];
// Endpoint to save subscriptions from clients
app.post('/api/subscribe', (req, res) => {
subscriptions.push(req.body);
res.status(201).json({});
});
// Endpoint to trigger a test notification
app.post('/api/trigger-notification', (req, res) => {
const { title, body, url } = req.body;
subscriptions.forEach(subscription => {
const payload = JSON.stringify({
title,
body,
data: { url } // Include URL in notification data
});
// Send push message
webpush.sendNotification(subscription, payload)
.catch(error => console.error('Error sending notification:', error));
});
res.status(200).json({ message: 'Notifications sent' });
});
// Serve static files from the client directory
app.use(express.static('../client'));
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on https://localhost:${PORT}`);
}); Step 4: Generate VAPID Keys#
VAPID (Voluntary Application Server Identification) is required to authenticate your server with browser push services. Generate keys using web-push:
cd server
npx web-push generate-vapid-keys Copy the public and private keys into a .env file in the server directory:
VAPID_PUBLIC_KEY=your_generated_public_key
VAPID_PRIVATE_KEY=your_generated_private_key Implementing Service Worker for Notification Clicks#
The service worker is critical for handling push events and notification clicks. It runs in the background, even when the browser is closed (if the service worker is installed).
client/service-worker.js#
This file handles two key events:
push: Displays the notification when a push message is received.notificationclick: Opens the specified URL when the notification is clicked.
// Listen for push events (server sends a push message)
self.addEventListener('push', (event) => {
const payload = event.data?.json() || { title: 'New Notification' };
const options = {
body: payload.body,
icon: '/icons/icon-192x192.png', // Optional: Custom icon
data: payload.data // Pass URL from server to the notification
};
// Show notification
event.waitUntil(
self.registration.showNotification(payload.title, options)
);
});
// Listen for notification click events
self.addEventListener('notificationclick', (event) => {
// Close the notification when clicked
event.notification.close();
// Open the specified URL
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}); Sending Push Notifications with URL Data#
To open a URL when the notification is clicked, your server must include the URL in the push payload’s data field (as shown in server.js).
Example: Trigger a notification with curl or Postman:
curl -X POST https://localhost:3000/api/trigger-notification \
-H "Content-Type: application/json" \
-d '{"title":"Hello!","body":"Click to visit our blog","url":"https://example.com/blog"}' Testing the Implementation#
Step 1: Run the Server with HTTPS#
Since push notifications require HTTPS, use mkcert to create trusted local certificates:
# Install mkcert (https://github.com/FiloSottile/mkcert)
mkcert -install
mkcert localhost
# Start server with HTTPS (using `https` module or tools like `serve`)
cd server
node server.js Step 2: Test the Client#
- Open
https://localhost:3000in Chrome. - Click "Subscribe to Notifications" and grant permission.
- Trigger a notification using the
curlcommand above. - A notification will appear in Chrome. Click it—it should open
https://example.com/blogin a new tab.
Step 3: Debug with Chrome DevTools#
- Service Workers: Go to
chrome://inspect/#service-workersor DevTools > Application > Service Workers to inspect the service worker. - Push Events: In DevTools > Application > Push, click "Push" to simulate a push event (use the payload
{"title":"Test","body":"Debug","data":{"url":"https://example.com"}}). - Notifications: Check DevTools > Application > Notifications to see active notifications.
Troubleshooting Common Issues#
1. URL Not Opening#
- Missing
data.url: Ensure the server includesdata: { url }in the push payload. - Service Worker Not Registered: Check DevTools > Application > Service Workers to confirm the service worker is active.
- HTTPS Issues: Push notifications won’t work on
http://localhost—usehttps://localhost.
2. Notification Not Showing#
- Permission Denied: Check if the user granted notification permission (DevTools > Application > Permissions).
userVisibleOnly: true: Required in the subscription (ensures notifications are visible to the user).
3. Service Worker Not Receiving Push Events#
- Subscription Expired: Push subscriptions can expire. Re-subscribe users periodically.
- Network Issues: Ensure the server can reach the browser’s push service (check firewall settings).
4. App Already Open#
To focus an existing tab instead of opening a new one, modify the notificationclick event:
self.addEventListener('notificationclick', (event) => {
event.notification.close();
// Focus or open the URL
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true })
.then(clientList => {
for (const client of clientList) {
if (client.url === event.notification.data.url && 'focus' in client) {
return client.focus();
}
}
return clients.openWindow(event.notification.data.url);
})
);
}); Conclusion#
By following this guide, you’ve learned how to implement Chrome push notifications that open a custom URL when clicked using service workers. Key steps include:
- Setting up a service worker to handle
pushandnotificationclickevents. - Including a
data.urlfield in the push payload from your server. - Using
clients.openWindow()in thenotificationclickevent to open the URL.
This functionality enhances user engagement by directing users to relevant content, improving retention and conversion.