Mobile apps have almost become indispensable to businesses because of their ability to provide users with the needed services quickly and with ease. However, what if I told you it is possible to have a mobile app-like experience on the web? This is what Progressive Web Apps (PWAs) are for. There is an increasing demand for mobile apps, as shown by the Top Design Firms survey. In 2022, almost half – 48% – of small businesses had their mobile apps compared to one-third 32% in 2021. This trend shows a shift in user needs demanding mobile-like interactions on all platforms. By developing a PWA, businesses can directly provide this native-app experience via the web browser, giving end users a dependable and engaging application without the need for a separate mobile application What are PWAs? A Progressive Web App is an app that provides users with native support, akin to mobile devices without, having to build with platform-specific considerations that vary from Android to IOS to desktops. They are lightweight, accessible on all devices, and automatically update when connected to the internet. Why consider a PWA for your Business? They are less expensive compared to developing native apps They can be easily installed on users’ home screens without an App store PWAs offer push notifications just like mobile apps They offer offline functionality by caching content They are designed to work on different devices Overview of Next.js’ new native support for PWAs I was recently researching the best possible to make my NextJs app a Progressive Web App when I came across the Reddit comment below about recent Next.js updates including native support for PWAs. Check out the comment here. After reviewing the update, I can say that it has never been easier to create PWAs and I’ll prove it. Project Setup and Requirements A reasonable understanding of React. NodeJs installed on your machine. PS: Two files are very important to get your app recognized as a PWA - the manifest file and the service worker file. Manifest file: The web app manifest is a file that contains information about our app that essentially makes it installable. This information includes the theme color, icons, starting URL, etc. Service worker: The service worker is a file containing the script that runs in the background of the application. It allows for features like push notifications, caching, and offline support. 1.) Setting up the App npx create-next-app@latest my-pwa-app Or yarn create next-app my-pwa-app After running the command, follow the prompts for your installation specifics then navigate into the project directory cd my-pwa-app Start up the dev server npm run dev 2.) Create the manifest file Create a manifest.json or manifest.ts file in the app directory. import type { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Next.js PWA', short_name: 'NextPWA', description: 'A Progressive Web App built with Next.js', start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#000000', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } } name: Official name of the app. This name appears on the home screen and allows users to identify the app. short_name: The short name appears when the name is long and there’s not much space to display description: Describes the application and what it does. start_url: When a user launches the app, it starts from the route specified here. display: The display property configures its appearance when launched and could be any of standalone minimal-ui fullscreen or browser background_color: It is important to use a background color that matches the app’s theme for smooth transition and user experience theme_color: This specifies the color of the UI elements like the browser’s toolbar and status bar. icons: The icons array contains properties of the icons to be used across different platforms, specifying the src, size , and type. For your PWA, it’s important to have at least icons with sizes 192x192 and 512x512 . Websites like manifest-generator can be used to generate the manifest file and different sizes of icon quickly 3.) Create the service worker file. The service worker would listen to push events and carry out the below function for push notifications self.addEventListener("push", function (event) { if (event.data) { const data = event.data.json(); const options = { body: data.body, icon: data.icon || '/icon.png', badge: "/icons/timer-icon-144.png", vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: "5", }, }; event.waitUntil(self.registration.showNotification(data.title, options)); } }); self.addEventListener("notificationclick", function (event) { console.log("Notification click received."); event.notification.close(); event.waitUntil(clients.openWindow("<https://your-website.com>")); //This should be the url to your website }); 4.) Register the service worker To register the service worker, you need to provide the route where the service worker is created. const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); Install web-push to handle notifications and subscriptions npm install web-push --save We then need to get the subscription if it the user has one or subscribe the user to push events. In a real application, this subscription should be sent to the server for storage async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } To generate the VAPID keys, run: npx web-push generate-vapid-keys Putting it all together for the UI with a function to test the push notification "use client"; import { useEffect, useState } from "react"; const Home = () => { const [subscription, setSubscription] = useState<PushSubscription | null>( null ); useEffect(() => { if ("serviceWorker" in navigator && "PushManager" in window) { registerServiceWorker(); } }, []); async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } //Create a function to test the notification const handleSendNotification = async () => { await fetch("/api/sendNotification", { method: "POST", body: JSON.stringify({ message: "Your timer has completed!", subscription: subscription, // This ideally, should not be included in the body. It should have already been saved on the server }), headers: { "Content-Type": "application/json", }, }); }; return ( <div> <h1>My PWA with Push Notifications</h1> <button onClick={handleSendNotification}>Notify Me!</button> </div> ); }; export default Home; 5.) Creating the endpoint Inside the app directory, create a new folder called api. Within the api folder, create another folder named sendNotification. Inside the sendNotification folder, create a file named route.ts. The structure should be as follows app/ └── api/ └── sendNotification/ └── route.ts In the route.ts file, include the following lines of code import { NextResponse } from "next/server"; import webpush from "web-push"; webpush.setVapidDetails( "mailto:your-email@example.com", // Your email process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ); export async function POST(req: Request) { const { message, subscription } = await req.json(); if (!subscription) { return NextResponse.json( { error: "No subscription available" }, { status: 400 } ); } try { await webpush.sendNotification( subscription, JSON.stringify({ title: "Notification", body: message }) ); return NextResponse.json({ success: true }); } catch (error) { console.error("Error sending notification:", error); return NextResponse.json( { error: "Failed to send notification" }, { status: 500 } ); } } To confirm the app is now a PWA, the download Icon should appear on the far right in the url bar. You should also be able to see information about the web manifest it in the applications tab. Conclusion Next.js’s recent update with native PWA support has made the process of creating progressive web apps very seamless. With Next.js, developing and deploying a PWA is now a straightforward process in part of modern web development as developers can now quickly create applications with the native-like features users expect, all from within a web browser. Mobile apps have almost become indispensable to businesses because of their ability to provide users with the needed services quickly and with ease. However, what if I told you it is possible to have a mobile app-like experience on the web? This is what Progressive Web Apps (PWAs) are for. There is an increasing demand for mobile apps, as shown by the Top Design Firms survey. In 2022, almost half – 48% – of small businesses had their mobile apps compared to one-third 32% in 2021. This trend shows a shift in user needs demanding mobile-like interactions on all platforms. Top Design Firms By developing a PWA, businesses can directly provide this native-app experience via the web browser, giving end users a dependable and engaging application without the need for a separate mobile application What are PWAs? A Progressive Web App is an app that provides users with native support, akin to mobile devices without, having to build with platform-specific considerations that vary from Android to IOS to desktops. They are lightweight, accessible on all devices, and automatically update when connected to the internet. Why consider a PWA for your Business? They are less expensive compared to developing native apps They can be easily installed on users’ home screens without an App store PWAs offer push notifications just like mobile apps They offer offline functionality by caching content They are designed to work on different devices They are less expensive compared to developing native apps They can be easily installed on users’ home screens without an App store PWAs offer push notifications just like mobile apps They offer offline functionality by caching content They are designed to work on different devices Overview of Next.js’ new native support for PWAs I was recently researching the best possible to make my NextJs app a Progressive Web App when I came across the Reddit comment below about recent Next.js updates including native support for PWAs. Check out the comment here . here After reviewing the update, I can say that it has never been easier to create PWAs and I’ll prove it. Project Setup and Requirements A reasonable understanding of React. NodeJs installed on your machine. A reasonable understanding of React. React. NodeJs installed on your machine. NodeJs PS: Two files are very important to get your app recognized as a PWA - the manifest file and the service worker file . manifest file service worker file Manifest file: The web app manifest is a file that contains information about our app that essentially makes it installable. This information includes the theme color, icons, starting URL, etc. Service worker: The service worker is a file containing the script that runs in the background of the application. It allows for features like push notifications, caching, and offline support. Manifest file: The web app manifest is a file that contains information about our app that essentially makes it installable. This information includes the theme color, icons, starting URL, etc. Manifest file: Service worker: The service worker is a file containing the script that runs in the background of the application. It allows for features like push notifications, caching, and offline support. Service worker: 1.) Setting up the App npx create-next-app@latest my-pwa-app npx create-next-app@latest my-pwa-app Or yarn create next-app my-pwa-app yarn create next-app my-pwa-app After running the command, follow the prompts for your installation specifics then navigate into the project directory cd my-pwa-app cd my-pwa-app Start up the dev server npm run dev npm run dev 2.) Create the manifest file Create a manifest.json or manifest.ts file in the app directory. manifest.json manifest.ts app import type { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Next.js PWA', short_name: 'NextPWA', description: 'A Progressive Web App built with Next.js', start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#000000', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } } import type { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Next.js PWA', short_name: 'NextPWA', description: 'A Progressive Web App built with Next.js', start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#000000', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } } name: Official name of the app. This name appears on the home screen and allows users to identify the app. short_name: The short name appears when the name is long and there’s not much space to display description: Describes the application and what it does. start_url: When a user launches the app, it starts from the route specified here. display: The display property configures its appearance when launched and could be any of standalone minimal-ui fullscreen or browser background_color: It is important to use a background color that matches the app’s theme for smooth transition and user experience theme_color: This specifies the color of the UI elements like the browser’s toolbar and status bar. icons: The icons array contains properties of the icons to be used across different platforms, specifying the src, size , and type. For your PWA, it’s important to have at least icons with sizes 192x192 and 512x512 . name: Official name of the app. This name appears on the home screen and allows users to identify the app. name: short_name : The short name appears when the name is long and there’s not much space to display short_name description : Describes the application and what it does. description start_url : When a user launches the app, it starts from the route specified here. start_url display : The display property configures its appearance when launched and could be any of standalone minimal-ui fullscreen or browser display standalone minimal-ui fullscreen browser background_color : It is important to use a background color that matches the app’s theme for smooth transition and user experience background_color theme_color : This specifies the color of the UI elements like the browser’s toolbar and status bar. theme_color icons : The icons array contains properties of the icons to be used across different platforms, specifying the src , size , and type . For your PWA, it’s important to have at least icons with sizes 192x192 and 512x512 . icons src size type 192x192 512x512 Websites like manifest-generator can be used to generate the manifest file and different sizes of icon quickly manifest-generator 3.) Create the service worker file. The service worker would listen to push events and carry out the below function for push notifications self.addEventListener("push", function (event) { if (event.data) { const data = event.data.json(); const options = { body: data.body, icon: data.icon || '/icon.png', badge: "/icons/timer-icon-144.png", vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: "5", }, }; event.waitUntil(self.registration.showNotification(data.title, options)); } }); self.addEventListener("notificationclick", function (event) { console.log("Notification click received."); event.notification.close(); event.waitUntil(clients.openWindow("<https://your-website.com>")); //This should be the url to your website }); self.addEventListener("push", function (event) { if (event.data) { const data = event.data.json(); const options = { body: data.body, icon: data.icon || '/icon.png', badge: "/icons/timer-icon-144.png", vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: "5", }, }; event.waitUntil(self.registration.showNotification(data.title, options)); } }); self.addEventListener("notificationclick", function (event) { console.log("Notification click received."); event.notification.close(); event.waitUntil(clients.openWindow("<https://your-website.com>")); //This should be the url to your website }); 4.) Register the service worker To register the service worker, you need to provide the route where the service worker is created. const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); Install web-push to handle notifications and subscriptions web-push npm install web-push --save npm install web-push --save We then need to get the subscription if it the user has one or subscribe the user to push events. In a real application, this subscription should be sent to the server for storage async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } To generate the VAPID keys, run: npx web-push generate-vapid-keys npx web-push generate-vapid-keys Putting it all together for the UI with a function to test the push notification "use client"; import { useEffect, useState } from "react"; const Home = () => { const [subscription, setSubscription] = useState<PushSubscription | null>( null ); useEffect(() => { if ("serviceWorker" in navigator && "PushManager" in window) { registerServiceWorker(); } }, []); async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } //Create a function to test the notification const handleSendNotification = async () => { await fetch("/api/sendNotification", { method: "POST", body: JSON.stringify({ message: "Your timer has completed!", subscription: subscription, // This ideally, should not be included in the body. It should have already been saved on the server }), headers: { "Content-Type": "application/json", }, }); }; return ( <div> <h1>My PWA with Push Notifications</h1> <button onClick={handleSendNotification}>Notify Me!</button> </div> ); }; export default Home; "use client"; import { useEffect, useState } from "react"; const Home = () => { const [subscription, setSubscription] = useState<PushSubscription | null>( null ); useEffect(() => { if ("serviceWorker" in navigator && "PushManager" in window) { registerServiceWorker(); } }, []); async function registerServiceWorker() { const registration = await navigator.serviceWorker.register("/sw.js", { //provide the route to the service worker scope: "/", updateViaCache: "none", }); const sub = await registration.pushManager.getSubscription(); if (sub) { setSubscription(sub); //This would be sent to a server } else { const pushSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY, // Your VAPID public key }); setSubscription(pushSubscription); } } //Create a function to test the notification const handleSendNotification = async () => { await fetch("/api/sendNotification", { method: "POST", body: JSON.stringify({ message: "Your timer has completed!", subscription: subscription, // This ideally, should not be included in the body. It should have already been saved on the server }), headers: { "Content-Type": "application/json", }, }); }; return ( <div> <h1>My PWA with Push Notifications</h1> <button onClick={handleSendNotification}>Notify Me!</button> </div> ); }; export default Home; 5.) Creating the endpoint Inside the app directory, create a new folder called api. Within the api folder, create another folder named sendNotification. Inside the sendNotification folder, create a file named route.ts. Inside the app directory, create a new folder called api . app api Within the api folder, create another folder named sendNotification . api sendNotification Inside the sendNotification folder, create a file named route.ts . sendNotification route.ts The structure should be as follows app/ └── api/ └── sendNotification/ └── route.ts app/ └── api/ └── sendNotification/ └── route.ts In the route.ts file, include the following lines of code import { NextResponse } from "next/server"; import webpush from "web-push"; webpush.setVapidDetails( "mailto:your-email@example.com", // Your email process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ); export async function POST(req: Request) { const { message, subscription } = await req.json(); if (!subscription) { return NextResponse.json( { error: "No subscription available" }, { status: 400 } ); } try { await webpush.sendNotification( subscription, JSON.stringify({ title: "Notification", body: message }) ); return NextResponse.json({ success: true }); } catch (error) { console.error("Error sending notification:", error); return NextResponse.json( { error: "Failed to send notification" }, { status: 500 } ); } } import { NextResponse } from "next/server"; import webpush from "web-push"; webpush.setVapidDetails( "mailto:your-email@example.com", // Your email process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! ); export async function POST(req: Request) { const { message, subscription } = await req.json(); if (!subscription) { return NextResponse.json( { error: "No subscription available" }, { status: 400 } ); } try { await webpush.sendNotification( subscription, JSON.stringify({ title: "Notification", body: message }) ); return NextResponse.json({ success: true }); } catch (error) { console.error("Error sending notification:", error); return NextResponse.json( { error: "Failed to send notification" }, { status: 500 } ); } } To confirm the app is now a PWA, the download Icon should appear on the far right in the url bar. You should also be able to see information about the web manifest it in the applications tab. Conclusion Next.js’s recent update with native PWA support has made the process of creating progressive web apps very seamless. With Next.js, developing and deploying a PWA is now a straightforward process in part of modern web development as developers can now quickly create applications with the native-like features users expect, all from within a web browser.