Declarative Web Push support (#2300)

* Declarative Web Push support, standardized JSON format

TODOs:
- sane app badge count

* URL backwards compatibility, add icon to the JSON payload, fix malformed payload recognition on classic push notifications

* typo: wrong app_badge placement in JSON payload

* adapt declarative JSON payload for legacy Push API using spec-conformant transformations
This commit is contained in:
soxa 2025-07-29 00:09:13 +02:00 committed by GitHub
parent 20147cae15
commit 9c8071339f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 23 additions and 7 deletions

View File

@ -15,14 +15,19 @@ function log (...args) {
function createPayload (notification) { function createPayload (notification) {
// https://web.dev/push-notifications-display-a-notification/#visual-options // 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 let { title, body, ...options } = notification
if (body) body = removeMd(body) if (body) body = removeMd(body)
return JSON.stringify({ return JSON.stringify({
title, web_push: 8030, // Declarative Web Push JSON format
options: { notification: {
title,
body, body,
timestamp: Date.now(), 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 ...options
} }
}) })
@ -58,6 +63,10 @@ async function sendNotification (subscription, payload) {
subject: process.env.VAPID_MAILTO, subject: process.env.VAPID_MAILTO,
publicKey: process.env.NEXT_PUBLIC_VAPID_PUBKEY, publicKey: process.env.NEXT_PUBLIC_VAPID_PUBKEY,
privateKey: process.env.VAPID_PRIVKEY privateKey: process.env.VAPID_PRIVKEY
},
// conformant to declarative web push spec
headers: {
'Content-Type': 'application/notification+json'
} }
}) })
.catch(async (err) => { .catch(async (err) => {
@ -95,7 +104,10 @@ async function sendUserNotification (userId, notification) {
} }
notification.data ??= {} notification.data ??= {}
if (notification.itemId) { 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) 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 delete notification.itemId
} }

View File

@ -73,13 +73,17 @@ setDefaultHandler(new NetworkOnly({
offlineFallback({ pageFallback: '/offline' }) offlineFallback({ pageFallback: '/offline' })
self.addEventListener('push', function (event) { self.addEventListener('push', function (event) {
let payload let title, options
try { try {
payload = event.data?.json() const { notification } = event.data?.json()
if (!payload) { if (!notification) {
throw new Error('no payload in push event') throw new Error('no payload in push event')
} }
// adapt declarative payload for legacy Push API
options = notification || {}
title = notification.title
} catch (err) { } catch (err) {
// we show a default nofication on any error because we *must* show a notification // 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 // else the browser will show one for us or worse, remove our push subscription
@ -94,7 +98,7 @@ self.addEventListener('push', function (event) {
} }
event.waitUntil( event.waitUntil(
self.registration.showNotification(payload.title, payload.options) self.registration.showNotification(title, options)
.then(() => self.registration.getNotifications()) .then(() => self.registration.getNotifications())
.then(notifications => self.navigator.setAppBadge?.(notifications.length)) .then(notifications => self.navigator.setAppBadge?.(notifications.length))
) )