diff --git a/server/types.go b/server/types.go
index 90995878..ba00b690 100644
--- a/server/types.go
+++ b/server/types.go
@@ -474,7 +474,7 @@ type apiWebPushUpdateSubscriptionRequest struct {
 	Topics   []string `json:"topics"`
 }
 
-// List of possible Web Push events
+// List of possible Web Push events (see sw.js)
 const (
 	webPushMessageEvent  = "message"
 	webPushExpiringEvent = "subscription_expiring"
@@ -486,8 +486,8 @@ type webPushPayload struct {
 	Message        *message `json:"message"`
 }
 
-func newWebPushPayload(subscriptionID string, message *message) webPushPayload {
-	return webPushPayload{
+func newWebPushPayload(subscriptionID string, message *message) *webPushPayload {
+	return &webPushPayload{
 		Event:          webPushMessageEvent,
 		SubscriptionID: subscriptionID,
 		Message:        message,
@@ -498,8 +498,8 @@ type webPushControlMessagePayload struct {
 	Event string `json:"event"`
 }
 
-func newWebPushSubscriptionExpiringPayload() webPushControlMessagePayload {
-	return webPushControlMessagePayload{
+func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload {
+	return &webPushControlMessagePayload{
 		Event: webPushExpiringEvent,
 	}
 }
diff --git a/web/public/sw.js b/web/public/sw.js
index bf6e8dbe..98ae3d8f 100644
--- a/web/public/sw.js
+++ b/web/public/sw.js
@@ -5,7 +5,7 @@ import { NetworkFirst } from "workbox-strategies";
 
 import { dbAsync } from "../src/app/db";
 
-import { getNotificationParams, icon, badge } from "../src/app/notificationUtils";
+import { toNotificationParams, icon, badge } from "../src/app/notificationUtils";
 
 import i18n from "../src/app/i18n";
 
@@ -40,50 +40,70 @@ const addNotification = async ({ subscriptionId, message }) => {
   self.navigator.setAppBadge?.(badgeCount);
 };
 
+/**
+ * Handle a received web push message and show notification.
+ *
+ * Since the service worker cannot play a sound, we send a broadcast to the web app, which (if it is running)
+ * receives the broadcast and plays a sound (see web/src/app/WebPush.js).
+ */
+const handlePushMessage = async (data) => {
+  const { subscription_id: subscriptionId, message } = data;
+
+  broadcastChannel.postMessage(message); // To potentially play sound
+
+  await addNotification({ subscriptionId, message });
+  await self.registration.showNotification(
+    ...toNotificationParams({
+      subscriptionId,
+      message,
+      defaultTitle: message.topic,
+      topicRoute: new URL(message.topic, self.location.origin).toString(),
+    })
+  );
+};
+
+/**
+ * Handle a received web push subscription expiring.
+ */
+const handlePushSubscriptionExpiring = async (data) => {
+  await self.registration.showNotification(i18n.t("web_push_subscription_expiring_title"), {
+    body: i18n.t("web_push_subscription_expiring_body"),
+    icon,
+    data,
+    badge,
+  });
+};
+
+/**
+ * Handle unknown push message. We can't ignore the push, since
+ * permission can be revoked by the browser.
+ */
+const handlePushUnknown = async (data) => {
+  await self.registration.showNotification(i18n.t("web_push_unknown_notification_title"), {
+    body: i18n.t("web_push_unknown_notification_body"),
+    icon,
+    data,
+    badge,
+  });
+};
+
 /**
  * Handle a received web push notification
  * @param {object} data see server/types.go, type webPushPayload
  */
 const handlePush = async (data) => {
-  if (data.event === "subscription_expiring") {
-    await self.registration.showNotification(i18n.t("web_push_subscription_expiring_title"), {
-      body: i18n.t("web_push_subscription_expiring_body"),
-      icon,
-      data,
-      badge,
-    });
-  } else if (data.event === "message") {
-    const { subscription_id: subscriptionId, message } = data;
-
-    // see: web/src/app/WebPush.js
-    // the service worker cannot play a sound, so if the web app
-    // is running, it receives the broadcast and plays it.
-    broadcastChannel.postMessage(message);
-
-    await addNotification({ subscriptionId, message });
-
-    await self.registration.showNotification(
-      ...getNotificationParams({
-        subscriptionId,
-        message,
-        defaultTitle: message.topic,
-        topicRoute: new URL(message.topic, self.location.origin).toString(),
-      })
-    );
+  if (data.event === "message") {
+    await handlePushMessage(data);
+  } else if (data.event === "subscription_expiring") {
+    await handlePushSubscriptionExpiring(data);
   } else {
-    // We can't ignore the push, since permission can be revoked by the browser
-    await self.registration.showNotification(i18n.t("web_push_unknown_notification_title"), {
-      body: i18n.t("web_push_unknown_notification_body"),
-      icon,
-      data,
-      badge,
-    });
+    await handlePushUnknown(data);
   }
 };
 
 /**
- * Handle a user clicking on the displayed notification from `showNotification`
- * This is also called when the user clicks on an action button
+ * Handle a user clicking on the displayed notification from `showNotification`.
+ * This is also called when the user clicks on an action button.
  */
 const handleClick = async (event) => {
   const clients = await self.clients.matchAll({ type: "window" });
@@ -195,7 +215,7 @@ self.addEventListener("notificationclick", (event) => {
   event.waitUntil(handleClick(event));
 });
 
-// see https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code
+// See https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code
 // self.__WB_MANIFEST is the workbox injection point that injects the manifest of the
 // vite dist files and their revision ids, for example:
 // [{"revision":"aaabbbcccdddeeefff12345","url":"/index.html"},...]
@@ -204,7 +224,7 @@ precacheAndRoute(
   self.__WB_MANIFEST
 );
 
-// delete any cached old dist files from previous service worker versions
+// Delete any cached old dist files from previous service worker versions
 cleanupOutdatedCaches();
 
 if (import.meta.env.MODE !== "development") {
diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js
index b0311f40..fa1498a3 100644
--- a/web/src/app/Notifier.js
+++ b/web/src/app/Notifier.js
@@ -1,5 +1,5 @@
 import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
-import { getNotificationParams } from "./notificationUtils";
+import { toNotificationParams } from "./notificationUtils";
 import prefs from "./Prefs";
 import routes from "../components/routes";
 
@@ -22,7 +22,7 @@ class Notifier {
 
     const registration = await this.serviceWorkerRegistration();
     await registration.showNotification(
-      ...getNotificationParams({
+      ...toNotificationParams({
         subscriptionId: subscription.id,
         message: notification,
         defaultTitle,
diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js
index 77437729..adc56318 100644
--- a/web/src/app/notificationUtils.js
+++ b/web/src/app/notificationUtils.js
@@ -39,7 +39,7 @@ const isImage = (filenameOrUrl) => filenameOrUrl?.match(/\.(png|jpe?g|gif|webp)$
 export const icon = "/static/images/ntfy.png";
 export const badge = "/static/images/mask-icon.svg";
 
-export const getNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => {
+export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => {
   const image = isImage(message.attachment?.name) ? message.attachment.url : undefined;
 
   // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API