Skip to content

Declarative Web Push support #2300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions lib/webPush.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ function log (...args) {

function createPayload (notification) {
// https://web.dev/push-notifications-display-a-notification/#visual-options
// https://webkit.org/blog/16535/meet-declarative-web-push/
// DEV: localhost in URLs is not supported by declarative web push
let { title, body, ...options } = notification
if (body) body = removeMd(body)
return JSON.stringify({
title,
options: {
web_push: 8030, // Declarative Web Push JSON format
notification: {
title,
body,
timestamp: Date.now(),
icon: '/icons/icon_x96.png',
icon: process.env.NEXT_PUBLIC_URL + '/icons/icon_x96.png',
navigate: process.env.NEXT_PUBLIC_URL + '/notifications', // navigate is required
app_badge: 1, // TODO: establish a proper badge count system
...options
}
})
Expand Down Expand Up @@ -58,6 +63,10 @@ async function sendNotification (subscription, payload) {
subject: process.env.VAPID_MAILTO,
publicKey: process.env.NEXT_PUBLIC_VAPID_PUBKEY,
privateKey: process.env.VAPID_PRIVKEY
},
// conformant to declarative web push spec
headers: {
'Content-Type': 'application/notification+json'
}
})
.catch(async (err) => {
Expand Down Expand Up @@ -95,7 +104,10 @@ async function sendUserNotification (userId, notification) {
}
notification.data ??= {}
if (notification.itemId) {
// legacy Push API notificationclick event needs data.url as the navigate key is consumed by the browser
notification.data.url ??= await createItemUrl(notification.itemId)
// Declarative Web Push can't use relative paths
notification.navigate ??= process.env.NEXT_PUBLIC_URL + notification.data.url
delete notification.itemId
}

Expand Down
12 changes: 8 additions & 4 deletions sw/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,17 @@ setDefaultHandler(new NetworkOnly({
offlineFallback({ pageFallback: '/offline' })

self.addEventListener('push', function (event) {
let payload
let title, options

try {
payload = event.data?.json()
if (!payload) {
const { notification } = event.data?.json()
if (!notification) {
throw new Error('no payload in push event')
}

// adapt declarative payload for legacy Push API
options = notification || {}
title = notification.title
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Service Worker Notification API Violation

The service worker incorrectly passes the entire notification object as options to self.registration.showNotification. This violates the Web Notifications API specification because the options object should not contain the title property (which is passed separately as the first argument) nor other declarative push properties (e.g., navigate, app_badge) unsupported by the legacy API. Furthermore, the destructuring of the push event payload can cause a TypeError if the payload is null or undefined, leading to a service worker crash.

Locations (1)

Fix in CursorFix in Web

Copy link
Member Author

@Soxasora Soxasora Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the ultra-rare case of having a notification that's not sent by us, the try catch handles this

stacker.news/sw/index.js

Lines 78 to 94 in a4a0fdb

try {
payload = event.data?.json()
if (!payload) {
throw new Error('no payload in push event')
}
} catch (err) {
// we show a default nofication on any error because we *must* show a notification
// else the browser will show one for us or worse, remove our push subscription
return event.waitUntil(
self.registration.showNotification(
// TODO: funny message as easter egg?
// example: "dude i'm bugging, that's wild" from https://www.youtube.com/watch?v=QsQLIaKK2s0&t=176s but in wild west theme?
'something went wrong',
{ icon: '/icons/icon_x96.png' }
)
)
}

} catch (err) {
// we show a default nofication on any error because we *must* show a notification
// else the browser will show one for us or worse, remove our push subscription
Expand All @@ -94,7 +98,7 @@ self.addEventListener('push', function (event) {
}

event.waitUntil(
self.registration.showNotification(payload.title, payload.options)
self.registration.showNotification(title, options)
.then(() => self.registration.getNotifications())
.then(notifications => self.navigator.setAppBadge?.(notifications.length))
)
Expand Down