Next.js course Β· Module 8: Deployment and Production
Progressive Web Apps and Service Workers
In this lesson11
Progressive Web Apps (PWA) are modern web applications that combine the best features of web and native applications. Next.js offers excellent support for PWAs, allowing the creation of fast, reliable, and engaging applications that also work offline. In this module, we will learn how to implement PWAs in Next.js using Service Workers.
What are Progressive Web Apps?
Progressive Web Apps are web applications that use modern web technologies to provide users with an experience similar to native applications. The key features of PWAs are:
- Progressive - work for every user, regardless of the browser
- Responsive - adapt to different screen sizes
- Connectivity Independent - work offline or with a poor connection
- App-like - provide navigation and interactions like in native applications
- Fresh - always up to date thanks to Service Worker
- Safe - served over HTTPS
- Discoverable - identified as "applications" by search engines
- Re-engageable - enable re-engagement via push notifications
- Installable - can be installed on a device without an app store
- Linkable - easy sharing via URL
Service Workers - the foundation of PWA
Service Workers are JavaScript scripts that run in the background, independently of the application's main thread. They allow:
- Caching resources for offline work
- Intercepting and modifying network requests
- Sending push notifications
- Background synchronization
- Application installation
Service Worker lifecycle
- Registration - registering the Service Worker
- Installation - installing and configuring the cache
- Activation - activating and clearing old caches
- Fetch - intercepting network requests
- Update - updating to a new version
PWA implementation in Next.js
1. Installing and configuring next-pwa
The easiest way to add PWA functionality to a Next.js application is to use the package next-pwa:
1npm install next-pwa
2# or
3yarn add next-pwaUpdate next.config.js:
1// next.config.js
2const withPWA = require('next-pwa')({
3 dest: 'public',
4 register: true,
5 skipWaiting: true,
6 disable: process.env.NODE_ENV === 'development', // Disable in development
7 runtimeCaching: [
8 {
9 urlPattern: /^https?.*/,
10 handler: 'NetworkFirst',
11 options: {
12 cacheName: 'offlineCache',
13 expiration: {
14 maxEntries: 200,
15 maxAgeSeconds: 24 * 60 * 60, // 24 hours
16 },
17 },
18 },
19 ],
20});
21
22module.exports = withPWA({
23 // Your standard Next.js configuration
24 reactStrictMode: true,
25 swcMinify: true,
26});2. Application manifest
The manifest is a JSON file that contains metadata about the PWA application. Create the file public/manifest.json:
1{
2 "name": "My PWA Application",
3 "short_name": "MyApp",
4 "description": "Example Progressive Web App built with Next.js",
5 "start_url": "/",
6 "display": "standalone",
7 "background_color": "#ffffff",
8 "theme_color": "#000000",
9 "orientation": "portrait-primary",
10 "icons": [
11 {
12 "src": "/icons/icon-72x72.png",
13 "sizes": "72x72",
14 "type": "image/png",
15 "purpose": "maskable any"
16 },
17 {
18 "src": "/icons/icon-96x96.png",
19 "sizes": "96x96",
20 "type": "image/png",
21 "purpose": "maskable any"
22 },
23 {
24 "src": "/icons/icon-128x128.png",
25 "sizes": "128x128",
26 "type": "image/png",
27 "purpose": "maskable any"
28 },
29 {
30 "src": "/icons/icon-144x144.png",
31 "sizes": "144x144",
32 "type": "image/png",
33 "purpose": "maskable any"
34 },
35 {
36 "src": "/icons/icon-152x152.png",
37 "sizes": "152x152",
38 "type": "image/png",
39 "purpose": "maskable any"
40 },
41 {
42 "src": "/icons/icon-192x192.png",
43 "sizes": "192x192",
44 "type": "image/png",
45 "purpose": "maskable any"
46 },
47 {
48 "src": "/icons/icon-384x384.png",
49 "sizes": "384x384",
50 "type": "image/png",
51 "purpose": "maskable any"
52 },
53 {
54 "src": "/icons/icon-512x512.png",
55 "sizes": "512x512",
56 "type": "image/png",
57 "purpose": "maskable any"
58 }
59 ],
60 "categories": ["productivity", "utilities"],
61 "screenshots": [
62 {
63 "src": "/screenshots/desktop.png",
64 "sizes": "1280x720",
65 "type": "image/png",
66 "form_factor": "wide"
67 },
68 {
69 "src": "/screenshots/mobile.png",
70 "sizes": "750x1334",
71 "type": "image/png",
72 "form_factor": "narrow"
73 }
74 ]
75}3. Adding meta tags to HTML
Update the main application layout to add the necessary meta tags:
1// app/layout.tsx
2import type { Metadata, Viewport } from 'next';
3
4export const metadata: Metadata = {
5 title: 'My PWA Application',
6 description: 'Example Progressive Web App',
7 generator: 'Next.js',
8 manifest: '/manifest.json',
9 keywords: ['nextjs', 'pwa', 'react'],
10 authors: [
11 { name: 'Your Team' },
12 ],
13 creator: 'Your Team',
14 formatDetection: {
15 email: false,
16 address: false,
17 telephone: false,
18 },
19 metadataBase: new URL('https://your-domain.com'),
20 alternates: {
21 canonical: '/',
22 },
23 openGraph: {
24 type: 'website',
25 siteName: 'My PWA Application',
26 title: {
27 default: 'My PWA Application',
28 template: '%s | My PWA Application',
29 },
30 description: 'Example Progressive Web App',
31 },
32 twitter: {
33 card: 'summary',
34 title: {
35 default: 'My PWA Application',
36 template: '%s | My PWA Application',
37 },
38 description: 'Example Progressive Web App',
39 },
40 appleWebApp: {
41 title: 'My PWA Application',
42 statusBarStyle: 'default',
43 capable: true,
44 },
45};
46
47export const viewport: Viewport = {
48 themeColor: [
49 { media: '(prefers-color-scheme: light)', color: '#ffffff' },
50 { media: '(prefers-color-scheme: dark)', color: '#000000' },
51 ],
52 minimumScale: 1,
53 initialScale: 1,
54 width: 'device-width',
55 shrinkToFit: false,
56 viewportFit: 'cover',
57};
58
59export default function RootLayout({
60 children,
61}: {
62 children: React.ReactNode;
63}) {
64 return (
65 <html lang="pl">
66 <head>
67 <link rel="icon" href="/favicon.ico" />
68 <link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
69 </head>
70 <body>
71 {children}
72 </body>
73 </html>
74 );
75}Custom Service Worker implementation
If you need more control over the Service Worker, you can create your own implementation:
1. Creating a Service Worker
1// public/sw.js
2const CACHE_NAME = 'my-app-v1';
3const urlsToCache = [
4 '/',
5 '/static/js/bundle.js',
6 '/static/css/main.css',
7 '/manifest.json',
8];
9
10// Install event - resource caching
11self.addEventListener('install', (event) => {
12 event.waitUntil(
13 caches.open(CACHE_NAME)
14 .then((cache) => {
15 console.log('Cache opened');
16 return cache.addAll(urlsToCache);
17 })
18 .then(() => {
19 // Forces activation of the new Service Worker
20 return self.skipWaiting();
21 })
22 );
23});
24
25// Activate event - clearing old caches
26self.addEventListener('activate', (event) => {
27 event.waitUntil(
28 caches.keys().then((cacheNames) => {
29 return Promise.all(
30 cacheNames.map((cacheName) => {
31 if (cacheName !== CACHE_NAME) {
32 console.log('Removing old cache:', cacheName);
33 return caches.delete(cacheName);
34 }
35 })
36 );
37 }).then(() => {
38 // Take control over all clients
39 return self.clients.claim();
40 })
41 );
42});
43
44// Fetch event - cache-first strategy
45self.addEventListener('fetch', (event) => {
46 event.respondWith(
47 caches.match(event.request)
48 .then((response) => {
49 // Return from cache if available
50 if (response) {
51 return response;
52 }
53
54 // Otherwise fetch from the network
55 return fetch(event.request).then((response) => {
56 // Check if we received a valid response
57 if (!response || response.status !== 200 || response.type !== 'basic') {
58 return response;
59 }
60
61 // Clone the response
62 const responseToCache = response.clone();
63
64 caches.open(CACHE_NAME)
65 .then((cache) => {
66 cache.put(event.request, responseToCache);
67 });
68
69 return response;
70 }).catch(() => {
71 // If the request failed, return the offline page
72 if (event.request.destination === 'document') {
73 return caches.match('/offline.html');
74 }
75 });
76 })
77 );
78});
79
80// Background sync
81self.addEventListener('sync', (event) => {
82 if (event.tag === 'background-sync') {
83 event.waitUntil(doBackgroundSync());
84 }
85});
86
87function doBackgroundSync() {
88 // Background synchronization implementation
89 return fetch('/api/sync')
90 .then(response => response.json())
91 .then(data => {
92 console.log('Background sync completed:', data);
93 })
94 .catch(err => {
95 console.error('Background sync failed:', err);
96 });
97}2. Registration Service Worker
1// lib/serviceWorker.ts
2export function registerSW() {
3 if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
4 window.addEventListener('load', () => {
5 navigator.serviceWorker.register('/sw.js')
6 .then((registration) => {
7 console.log('SW registered: ', registration);
8
9 // Checking for updates
10 registration.addEventListener('updatefound', () => {
11 const newWorker = registration.installing;
12 if (newWorker) {
13 newWorker.addEventListener('statechange', () => {
14 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
15 // A new Service Worker is available
16 showUpdateAvailableNotification();
17 }
18 });
19 }
20 });
21 })
22 .catch((error) => {
23 console.log('SW registration failed: ', error);
24 });
25 });
26 }
27}
28
29export function unregisterSW() {
30 if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
31 navigator.serviceWorker.ready.then((registration) => {
32 registration.unregister();
33 });
34 }
35}
36
37function showUpdateAvailableNotification() {
38 // Show notification about available update
39 if (confirm('A new version of the application is available. Would you like to update it?')) {
40 window.location.reload();
41 }
42}3. Usage in a component
1// app/page.tsx
2'use client';
3
4import { useEffect } from 'react';
5import { registerSW } from '@/lib/serviceWorker';
6
7export default function HomePage() {
8 useEffect(() => {
9 registerSW();
10 }, []);
11
12 return (
13 <div>
14 <h1>My PWA Application</h1>
15 <p>Welcome to the Progressive Web App!</p>
16 </div>
17 );
18}Advanced caching strategies
Different content types require different caching strategies:
1. Cache First - for static resources
1// Cache First strategy
2self.addEventListener('fetch', (event) => {
3 if (event.request.destination === 'image' ||
4 event.request.destination === 'style' ||
5 event.request.destination === 'script') {
6 event.respondWith(
7 caches.match(event.request).then((cachedResponse) => {
8 return cachedResponse || fetch(event.request).then((response) => {
9 const responseClone = response.clone();
10 caches.open(CACHE_NAME).then((cache) => {
11 cache.put(event.request, responseClone);
12 });
13 return response;
14 });
15 })
16 );
17 }
18});2. Network First - for dynamic content
1// Network First strategy
2self.addEventListener('fetch', (event) => {
3 if (event.request.url.includes('/api/')) {
4 event.respondWith(
5 fetch(event.request)
6 .then((response) => {
7 const responseClone = response.clone();
8 caches.open(CACHE_NAME).then((cache) => {
9 cache.put(event.request, responseClone);
10 });
11 return response;
12 })
13 .catch(() => {
14 return caches.match(event.request);
15 })
16 );
17 }
18});3. Stale While Revalidate - for frequently changing content
1// Stale While Revalidate strategy
2self.addEventListener('fetch', (event) => {
3 if (event.request.url.includes('/news/')) {
4 event.respondWith(
5 caches.match(event.request).then((cachedResponse) => {
6 const fetchPromise = fetch(event.request).then((networkResponse) => {
7 caches.open(CACHE_NAME).then((cache) => {
8 cache.put(event.request, networkResponse.clone());
9 });
10 return networkResponse;
11 });
12
13 return cachedResponse || fetchPromise;
14 })
15 );
16 }
17});Implementing offline functionality
1. Offline page
Create a dedicated offline page:
1// app/offline/page.tsx
2export default function OfflinePage() {
3 return (
4 <div className="min-h-screen flex items-center justify-center bg-gray-100">
5 <div className="text-center">
6 <div className="mb-4">
7 <svg className="mx-auto h-16 w-16 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 2.25a9.75 9.75 0 1 0 0 19.5 9.75 9.75 0 0 0 0-19.5Z" />
9 </svg>
10 </div>
11 <h1 className="text-2xl font-bold text-gray-900 mb-2">You are offline</h1>
12 <p className="text-gray-600 mb-4">
13 We cannot connect to the internet. Check your connection and try again.
14 </p>
15 <button
16 onClick={() => window.location.reload()}
17 className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
18 >
19 Try again
20 </button>
21 </div>
22 </div>
23 );
24}2. Detecting online/offline state
1// hooks/useOnlineStatus.ts
2import { useState, useEffect } from 'react';
3
4export function useOnlineStatus() {
5 const [isOnline, setIsOnline] = useState(true);
6
7 useEffect(() => {
8 function handleOnline() {
9 setIsOnline(true);
10 }
11
12 function handleOffline() {
13 setIsOnline(false);
14 }
15
16 window.addEventListener('online', handleOnline);
17 window.addEventListener('offline', handleOffline);
18
19 // Check initial state
20 setIsOnline(navigator.onLine);
21
22 return () => {
23 window.removeEventListener('online', handleOnline);
24 window.removeEventListener('offline', handleOffline);
25 };
26 }, []);
27
28 return isOnline;
29}3. Connection status indicator component
1// components/OnlineStatusIndicator.tsx
2'use client';
3
4import { useOnlineStatus } from '@/hooks/useOnlineStatus';
5
6export default function OnlineStatusIndicator() {
7 const isOnline = useOnlineStatus();
8
9 if (isOnline) {
10 return null; // Don't show anything when there is a connection
11 }
12
13 return (
14 <div className="fixed top-0 left-0 right-0 bg-red-500 text-white text-center py-2 z-50">
15 <span className="text-sm font-medium">
16 No internet connection - you are operating in offline mode
17 </span>
18 </div>
19 );
20}Push Notifications
Push notifications allow user re-engagement:
1. Subscription registration
1// lib/notifications.ts
2
3// Function to convert VAPID key
4function urlBase64ToUint8Array(base64String: string) {
5 const padding = '='.repeat((4 - base64String.length % 4) % 4);
6 const base64 = (base64String + padding)
7 .replace(/\-/g, '+')
8 .replace(/_/g, '/');
9
10 const rawData = window.atob(base64);
11 const outputArray = new Uint8Array(rawData.length);
12
13 for (let i = 0; i < rawData.length; ++i) {
14 outputArray[i] = rawData.charCodeAt(i);
15 }
16 return outputArray;
17}
18
19export async function subscribeToPushNotifications() {
20 if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
21 throw new Error('Push notifications are not supported');
22 }
23
24 const registration = await navigator.serviceWorker.ready;
25
26 // Check if a subscription already exists
27 let subscription = await registration.pushManager.getSubscription();
28
29 if (!subscription) {
30 const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
31 if (!vapidPublicKey) {
32 throw new Error('VAPID public key is not configured');
33 }
34
35 subscription = await registration.pushManager.subscribe({
36 userVisibleOnly: true,
37 applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
38 });
39 }
40
41 // Send subscription to the server
42 await fetch('/api/notifications/subscribe', {
43 method: 'POST',
44 headers: {
45 'Content-Type': 'application/json',
46 },
47 body: JSON.stringify(subscription),
48 });
49
50 return subscription;
51}
52
53export async function unsubscribeFromPushNotifications() {
54 const registration = await navigator.serviceWorker.ready;
55 const subscription = await registration.pushManager.getSubscription();
56
57 if (subscription) {
58 await subscription.unsubscribe();
59
60 // Notify the server about subscription cancellation
61 await fetch('/api/notifications/unsubscribe', {
62 method: 'POST',
63 headers: {
64 'Content-Type': 'application/json',
65 },
66 body: JSON.stringify(subscription),
67 });
68 }
69}2. Handling push notifications in Service Worker
1// public/sw.js (addition to previous code)
2
3// Handling push notifications
4self.addEventListener('push', (event) => {
5 const options = {
6 body: 'You have a new message!',
7 icon: '/icons/icon-192x192.png',
8 badge: '/icons/badge-72x72.png',
9 vibrate: [100, 50, 100],
10 data: {
11 dateOfArrival: Date.now(),
12 primaryKey: 1
13 },
14 actions: [
15 {
16 action: 'explore',
17 title: 'See more',
18 icon: '/icons/checkmark.png'
19 },
20 {
21 action: 'close',
22 title: 'Close',
23 icon: '/icons/xmark.png'
24 }
25 ]
26 };
27
28 if (event.data) {
29 const data = event.data.json();
30 options.body = data.body || options.body;
31 options.data = { ...options.data, ...data.data };
32 }
33
34 event.waitUntil(
35 self.registration.showNotification('My PWA Application', options)
36 );
37});
38
39// Handling notification click
40self.addEventListener('notificationclick', (event) => {
41 event.notification.close();
42
43 if (event.action === 'explore') {
44 // Open a specific page
45 event.waitUntil(
46 clients.openWindow('/notifications')
47 );
48 } else if (event.action === 'close') {
49 // Close the notification
50 return;
51 } else {
52 // Default action - open the main page
53 event.waitUntil(
54 clients.openWindow('/')
55 );
56 }
57});3. API endpoint for sending notifications
1// app/api/notifications/send/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import webpush from 'web-push';
4import { prisma } from '@/lib/prisma';
5
6// VAPID configuration
7webpush.setVapidDetails(
8 'mailto:your-email@example.com',
9 process.env.VAPID_PUBLIC_KEY!,
10 process.env.VAPID_PRIVATE_KEY!
11);
12
13export async function POST(request: NextRequest) {
14 try {
15 const { title, body, url } = await request.json();
16
17 // Fetch all subscriptions from the database
18 const subscriptions = await prisma.pushSubscription.findMany();
19
20 const payload = JSON.stringify({
21 title,
22 body,
23 url,
24 icon: '/icons/icon-192x192.png',
25 });
26
27 // Send notifications to all subscriptions
28 const promises = subscriptions.map(async (subscription) => {
29 try {
30 await webpush.sendNotification(
31 {
32 endpoint: subscription.endpoint,
33 keys: {
34 p256dh: subscription.p256dh,
35 auth: subscription.auth,
36 },
37 },
38 payload
39 );
40 } catch (error) {
41 console.error('Error sending notification:', error);
42
43 // If the subscription is invalid, remove it
44 if (error.statusCode === 410) {
45 await prisma.pushSubscription.delete({
46 where: { id: subscription.id },
47 });
48 }
49 }
50 });
51
52 await Promise.all(promises);
53
54 return NextResponse.json({ success: true });
55 } catch (error) {
56 console.error('Error sending notifications:', error);
57 return NextResponse.json(
58 { error: 'Failed to send notifications' },
59 { status: 500 }
60 );
61 }
62}Installing the application (Add to Home Screen)
1. InstallPrompt component
1// components/InstallPrompt.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5
6interface BeforeInstallPromptEvent extends Event {
7 readonly platforms: string[];
8 readonly userChoice: Promise<{
9 outcome: 'accepted' | 'dismissed';
10 platform: string;
11 }>;
12 prompt(): Promise<void>;
13}
14
15export default function InstallPrompt() {
16 const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
17 const [showInstallButton, setShowInstallButton] = useState(false);
18
19 useEffect(() => {
20 const handler = (e: Event) => {
21 // Prevent automatic prompt display
22 e.preventDefault();
23 setDeferredPrompt(e as BeforeInstallPromptEvent);
24 setShowInstallButton(true);
25 };
26
27 window.addEventListener('beforeinstallprompt', handler);
28
29 // Check if the application is already installed
30 window.addEventListener('appinstalled', () => {
31 console.log('PWA has been installed');
32 setShowInstallButton(false);
33 setDeferredPrompt(null);
34 });
35
36 return () => {
37 window.removeEventListener('beforeinstallprompt', handler);
38 };
39 }, []);
40
41 const handleInstallClick = async () => {
42 if (!deferredPrompt) return;
43
44 // Show the installation prompt
45 deferredPrompt.prompt();
46
47 // Wait for user choice
48 const { outcome } = await deferredPrompt.userChoice;
49
50 if (outcome === 'accepted') {
51 console.log('User accepted the installation');
52 } else {
53 console.log('User rejected the installation');
54 }
55
56 setDeferredPrompt(null);
57 setShowInstallButton(false);
58 };
59
60 if (!showInstallButton) {
61 return null;
62 }
63
64 return (
65 <div className="fixed bottom-4 right-4 bg-blue-500 text-white p-4 rounded-lg shadow-lg">
66 <div className="flex items-center space-x-3">
67 <div>
68 <h3 className="font-semibold">Install application</h3>
69 <p className="text-sm opacity-90">Add to home screen for faster access</p>
70 </div>
71 <div className="flex space-x-2">
72 <button
73 onClick={handleInstallClick}
74 className="bg-white text-blue-500 px-3 py-1 rounded text-sm font-medium"
75 >
76 Install
77 </button>
78 <button
79 onClick={() => setShowInstallButton(false)}
80 className="text-white opacity-75 hover:opacity-100"
81 >
82 Γ
83 </button>
84 </div>
85 </div>
86 </div>
87 );
88}Testing PWA
1. Lighthouse Audit
Lighthouse is a Google tool for PWA audits. You can run it:
- In Chrome DevTools (Lighthouse tab)
- From the command line:
npm install -g lighthouse - Online: https://web.dev/measure/
2. PWA Builder
Microsoft PWA Builder (https://www.pwabuilder.com/) is a tool for testing and building PWAs.
3. Manual testing
1// utils/pwa-test.ts
2export function testPWAFeatures() {
3 const results = {
4 serviceWorker: 'serviceWorker' in navigator,
5 pushNotifications: 'PushManager' in window,
6 notifications: 'Notification' in window,
7 backgroundSync: 'serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype,
8 installPrompt: true, // Detected dynamically
9 offline: 'onLine' in navigator,
10 };
11
12 console.table(results);
13 return results;
14}PWA best practices
1. Performance
- Use appropriate caching strategies
- Optimize bundle size
- Implement lazy loading
- Use Service Worker to preload critical resources
2. User Experience
- Ensure smooth transitions between online/offline modes
- Implement skeleton screens during loading
- Add loading indicators
- Provide clear connection status messages
3. Security
- Always use HTTPS
- Validate all input data
- Implement Content Security Policy (CSP)
- Regularly update the Service Worker
4. Accessibility
- Provide screen reader support
- Use appropriate color contrasts
- Implement keyboard navigation
- Add appropriate aria-labels
Summary
Progressive Web Apps are a powerful technology that allows creating web applications with functionality close to native applications. Next.js with the next-pwa package or a custom Service Worker implementation offers all the tools needed to create a fully functional PWA.
Key elements of a PWA implementation in Next.js:
- Service Worker - for caching and offline work
- Web App Manifest - for application metadata
- HTTPS - required for PWA
- Responsive Design - for different devices
- Push Notifications - for user engagement
- Install Prompts - for installation on a device
PWAs allow delivering a better user experience, increasing engagement, and improving application performance, especially in conditions of poor internet connection.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: Progressive Web Apps and Service Workers
4interface PWAFeature {
5 id: string;
6 name: string;
7 icon: string;
8 description: string;
9 status: 'supported' | 'partial' | 'not-supported';
10 code: string;
11}
12
13function PWADemo() {
14 const [activeTab, setActiveTab] = useState<'features' | 'manifest' | 'service-worker' | 'install'>('features');
15 const [installStep, setInstallStep] = useState(0);
16 const [cacheStrategy, setCacheStrategy] = useState<'cache-first' | 'network-first' | 'stale-while-revalidate'>('cache-first');
17
18 const features: PWAFeature[] = [
19 { id: 'offline', name: 'Offline Support', icon: 'OFF', description: 'Works without an internet connection',
20 status: 'supported', code: "// Service Worker cache\nself.addEventListener('fetch', (event) => {\n event.respondWith(\n caches.match(event.request)\n .then(cached => cached || fetch(event.request))\n );\n});" },
21 { id: 'install', name: 'Installation', icon: 'INS', description: 'Can be added to the home screen',
22 status: 'supported', code: "// manifest.json\n{\n \"name\": \"My PWA\",\n \"short_name\": \"PWA\",\n \"start_url\": \"/\",\n \"display\": \"standalone\",\n \"background_color\": \"#0f0f23\",\n \"theme_color\": \"#64ffda\"\n}" },
23 { id: 'push', name: 'Push Notifications', icon: 'PUS', description: 'Notifications even when the app is closed',
24 status: 'supported', code: "// Subscribe to push\nconst registration = await navigator.serviceWorker.ready;\nconst subscription = await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: VAPID_PUBLIC_KEY,\n});" },
25 { id: 'sync', name: 'Background Sync', icon: 'SYN', description: 'Background data synchronization',
26 status: 'partial', code: "// Background sync\nnavigator.serviceWorker.ready.then(reg => {\n reg.sync.register('sync-data');\n});" },
27 { id: 'share', name: 'Web Share API', icon: 'SHR', description: 'Native content sharing',
28 status: 'supported', code: "// Share API\nawait navigator.share({\n title: 'My App',\n text: 'Check this out!',\n url: window.location.href,\n});" },
29 ];
30
31 const [selectedFeature, setSelectedFeature] = useState(features[0]);
32
33 const statusColors: Record<string, string> = { supported: '#10b981', partial: '#f59e0b', 'not-supported': '#ef4444' };
34
35 const manifest = {
36 name: "Quantum City PWA",
37 short_name: "QuantumPWA",
38 description: "Advanced PWA application",
39 start_url: "/",
40 display: "standalone",
41 background_color: "#0f0f23",
42 theme_color: "#64ffda",
43 icons: [
44 { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
45 { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
46 ],
47 };
48
49 const cacheStrategies: Record<string, { desc: string; code: string }> = {
50 'cache-first': {
51 desc: 'Checks the cache first, then the network',
52 code: "event.respondWith(\n caches.match(event.request).then(cached =>\n cached || fetch(event.request).then(response => {\n const clone = response.clone();\n caches.open('v1').then(cache => cache.put(event.request, clone));\n return response;\n })\n )\n);",
53 },
54 'network-first': {
55 desc: 'Network first, cache as fallback',
56 code: "event.respondWith(\n fetch(event.request)\n .then(response => {\n const clone = response.clone();\n caches.open('v1').then(cache => cache.put(event.request, clone));\n return response;\n })\n .catch(() => caches.match(event.request))\n);",
57 },
58 'stale-while-revalidate': {
59 desc: 'Instantly from cache, updates in the background',
60 code: "event.respondWith(\n caches.open('v1').then(cache =>\n cache.match(event.request).then(cached => {\n const fetched = fetch(event.request)\n .then(response => {\n cache.put(event.request, response.clone());\n return response;\n });\n return cached || fetched;\n })\n )\n);",
61 },
62 };
63
64 const installSteps = [
65 { title: 'next-pwa', desc: 'npm install next-pwa' },
66 { title: 'next.config.js', desc: 'next-pwa configuration in next.config.js' },
67 { title: 'manifest.json', desc: 'Create the public/manifest.json file' },
68 { title: 'Icons', desc: 'Add 192x192 and 512x512 icons' },
69 { title: 'Meta tags', desc: 'Add PWA meta tags in layout.tsx' },
70 { title: 'Done!', desc: 'The PWA application is ready' },
71 ];
72
73 return (
74 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
75 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>PWA & Service Workers</h1>
76 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Progressive Web Apps in Next.js</p>
77
78 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
79 {[
80 { id: 'features' as const, label: 'PWA Features' },
81 { id: 'manifest' as const, label: 'Manifest' },
82 { id: 'service-worker' as const, label: 'Service Worker' },
83 { id: 'install' as const, label: 'Installation' },
84 ].map(tab => (
85 <button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
86 padding: '10px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
87 background: activeTab === tab.id ? '#64ffda' : '#1a1a2e', color: activeTab === tab.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
88 }}>{tab.label}</button>
89 ))}
90 </div>
91
92 {activeTab === 'features' && (
93 <div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: '20px' }}>
94 <div style={{ display: 'grid', gap: '8px', alignContent: 'start' }}>
95 {features.map(f => (
96 <div key={f.id} onClick={() => setSelectedFeature(f)} style={{
97 padding: '12px', borderRadius: '8px', cursor: 'pointer',
98 background: selectedFeature.id === f.id ? '#1a2a3e' : '#1a1a2e',
99 border: selectedFeature.id === f.id ? '1px solid #64ffda' : '1px solid #333',
100 }}>
101 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
102 <strong>{f.name}</strong>
103 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '12px', background: statusColors[f.status] + '30', color: statusColors[f.status] }}>{f.status}</span>
104 </div>
105 <div style={{ fontSize: '12px', color: '#8892b0', marginTop: '4px' }}>{f.description}</div>
106 </div>
107 ))}
108 </div>
109 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
110 <h3 style={{ color: '#64ffda', marginTop: 0 }}>{selectedFeature.name}</h3>
111 <p style={{ color: '#8892b0' }}>{selectedFeature.description}</p>
112 <pre style={{ background: '#0f0f23', padding: '12px', borderRadius: '6px', fontSize: '12px', color: '#e0e0e0', overflow: 'auto', whiteSpace: 'pre-wrap' }}>{selectedFeature.code}</pre>
113 </div>
114 </div>
115 )}
116
117 {activeTab === 'manifest' && (
118 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
119 <h3 style={{ color: '#64ffda', marginTop: 0 }}>manifest.json</h3>
120 <pre style={{ background: '#0f0f23', padding: '16px', borderRadius: '8px', fontSize: '13px', color: '#e0e0e0', overflow: 'auto' }}>
121 {JSON.stringify(manifest, null, 2)}
122 </pre>
123 </div>
124 )}
125
126 {activeTab === 'service-worker' && (
127 <div>
128 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
129 {(Object.keys(cacheStrategies) as Array<keyof typeof cacheStrategies>).map(s => (
130 <button key={s} onClick={() => setCacheStrategy(s as any)} style={{
131 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
132 background: cacheStrategy === s ? '#7c3aed' : '#1a1a2e', color: '#fff',
133 }}>{s}</button>
134 ))}
135 </div>
136 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
137 <p style={{ color: '#8892b0' }}>{cacheStrategies[cacheStrategy].desc}</p>
138 <pre style={{ background: '#0f0f23', padding: '16px', borderRadius: '8px', fontSize: '12px', color: '#e0e0e0', overflow: 'auto', whiteSpace: 'pre-wrap' }}>{cacheStrategies[cacheStrategy].code}</pre>
139 </div>
140 </div>
141 )}
142
143 {activeTab === 'install' && (
144 <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
145 <h3 style={{ color: '#64ffda', marginTop: 0 }}>PWA Configuration in Next.js</h3>
146 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px', flexWrap: 'wrap' }}>
147 {installSteps.map((s, i) => (
148 <button key={i} onClick={() => setInstallStep(i)} style={{
149 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer', fontSize: '12px',
150 background: installStep === i ? '#64ffda' : i < installStep ? '#10b98130' : '#333',
151 color: installStep === i ? '#0f0f23' : '#8892b0',
152 }}>{i + 1}. {s.title}</button>
153 ))}
154 </div>
155 <div style={{ padding: '16px', background: '#0f0f23', borderRadius: '8px' }}>
156 <strong style={{ color: '#64ffda' }}>{installSteps[installStep].title}</strong>
157 <p style={{ color: '#8892b0', marginBottom: 0 }}>{installSteps[installStep].desc}</p>
158 </div>
159 </div>
160 )}
161 </div>
162 );
163}
164
165export default PWADemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What must a Next.js application include to qualify as a Progressive Web App?