cyberangles blog

Chrome Push Notifications: How to Open a URL When Clicked (Service Worker Implementation Guide)

Push notifications are a powerful tool for re-engaging users, driving traffic, and keeping your audience informed in real time. However, their effectiveness hinges on what happens when a user clicks them. By default, a notification might simply dismiss itself, but with a little setup, you can direct users to a specific URL—whether it’s a blog post, product page, or app dashboard.

In this guide, we’ll walk through how to implement Chrome push notifications that open a custom URL when clicked, using service workers. We’ll cover everything from setting up your project to handling notification clicks, with detailed code examples and troubleshooting tips.

2026-02

Table of Contents#

  1. Prerequisites
  2. Understanding the Push Notifications Flow
  3. Setting Up the Project
  4. Implementing the Service Worker
  5. Sending Push Notifications with URL Data
  6. Testing the Implementation
  7. Troubleshooting Common Issues
  8. Conclusion
  9. 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 localhost with 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 push and notificationclick).
  • 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:

  1. User Grants Permission: The user allows your site to send notifications.
  2. Subscription: The client (browser) generates a push subscription object (containing an endpoint URL provided by the browser’s push service).
  3. Store Subscription: Send this subscription to your backend server and store it (e.g., in a database).
  4. 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).
  5. Push Service Delivers Message: The browser’s push service forwards the message to the client’s service worker.
  6. Service Worker Shows Notification: The service worker listens for the push event, then displays a notification.
  7. Handle Click: When the user clicks the notification, the service worker listens for the notificationclick event 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#

  1. Open https://localhost:3000 in Chrome.
  2. Click "Subscribe to Notifications" and grant permission.
  3. Trigger a notification using the curl command above.
  4. A notification will appear in Chrome. Click it—it should open https://example.com/blog in a new tab.

Step 3: Debug with Chrome DevTools#

  • Service Workers: Go to chrome://inspect/#service-workers or 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 includes data: { 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—use https://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:

  1. Setting up a service worker to handle push and notificationclick events.
  2. Including a data.url field in the push payload from your server.
  3. Using clients.openWindow() in the notificationclick event to open the URL.

This functionality enhances user engagement by directing users to relevant content, improving retention and conversion.

References#