progressive-web-app
About
This skill enables developers to build Progressive Web Apps (PWAs) with core capabilities including service workers, web app manifests, and offline support. Use it when creating app-like web experiences that require installability, fast loading, and mobile functionality. It provides the essential tools for implementing features like push notifications and standalone app displays.
Documentation
Progressive Web App
Overview
Build progressive web applications with offline support, installability, service workers, and web app manifests to deliver app-like experiences in the browser.
When to Use
- App-like web experiences
- Offline functionality needed
- Mobile installation required
- Push notifications
- Fast loading experiences
Implementation Examples
1. Web App Manifest
// public/manifest.json
{
"name": "My Awesome App",
"short_name": "AwesomeApp",
"description": "A progressive web application",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/images/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/images/screenshot-1.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/images/screenshot-2.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["productivity", "utilities"],
"shortcuts": [
{
"name": "Quick Note",
"short_name": "Note",
"description": "Create a quick note",
"url": "/new-note",
"icons": [
{
"src": "/images/note-icon.png",
"sizes": "192x192"
}
]
}
]
}
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#007bff">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/images/icon-192.png">
<title>My Awesome App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
2. Service Worker Implementation
// public/service-worker.ts
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/css/main.css',
'/js/app.js',
'/images/icon-192.png',
'/offline.html'
];
// Install event
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate event
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch event with cache-first strategy for static assets
self.addEventListener('fetch', (event: FetchEvent) => {
const { request } = event;
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Cache first for static assets
if (request.destination === 'image' || request.destination === 'font') {
event.respondWith(
caches.match(request).then(response => {
return response || fetch(request).then(res => {
if (res.ok) {
const clone = res.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, clone);
});
}
return res;
});
}).catch(() => {
return caches.match('/offline.html');
})
);
}
// Network first for API calls
if (request.url.includes('/api/')) {
event.respondWith(
fetch(request)
.then(response => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request);
})
);
}
// Stale while revalidate for HTML
if (request.destination === 'document') {
event.respondWith(
caches.match(request).then(cachedResponse => {
const fetchPromise = fetch(request).then(response => {
if (response.ok) {
caches.open(CACHE_NAME).then(cache => {
cache.put(request, response.clone());
});
}
return response;
});
return cachedResponse || fetchPromise;
})
);
}
});
// Background Sync
self.addEventListener('sync', (event: any) => {
if (event.tag === 'sync-notes') {
event.waitUntil(syncNotes());
}
});
async function syncNotes() {
const db = await openDB('notes');
const unsynced = await db.getAll('keyval', IDBKeyRange.bound('pending_', 'pending_\uffff'));
for (const item of unsynced) {
try {
await fetch('/api/notes', {
method: 'POST',
body: JSON.stringify(item.value)
});
await db.delete('keyval', item.key);
} catch (error) {
console.error('Sync failed:', error);
}
}
}
3. Install Prompt and App Installation
// hooks/useInstallPrompt.ts
import { useState, useEffect } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export const useInstallPrompt = () => {
const [promptEvent, setPromptEvent] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
const [isIOSInstalled, setIsIOSInstalled] = useState(false);
useEffect(() => {
const handleBeforeInstallPrompt = (e: Event) => {
e.preventDefault();
setPromptEvent(e as BeforeInstallPromptEvent);
};
const handleAppInstalled = () => {
setIsInstalled(true);
setPromptEvent(null);
};
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
// Check if running as installed app
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
// Check iOS
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent);
const isIOSApp = navigator.standalone === true;
if (isIOSDevice && !isIOSApp) {
setIsIOSInstalled(false);
}
return () => {
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
};
}, []);
const installApp = async () => {
if (promptEvent) {
await promptEvent.prompt();
const { outcome } = await promptEvent.userChoice;
if (outcome === 'accepted') {
setIsInstalled(true);
}
setPromptEvent(null);
}
};
return {
promptEvent,
canInstall: promptEvent !== null,
isInstalled,
isIOSInstalled,
installApp
};
};
// components/InstallPrompt.tsx
export const InstallPrompt: React.FC = () => {
const { canInstall, isInstalled, installApp } = useInstallPrompt();
if (isInstalled || !canInstall) return null;
return (
<div className="install-prompt">
<h2>Install App</h2>
<p>Install our app for quick access and offline support</p>
<button onClick={installApp}>Install</button>
</div>
);
};
4. Offline Support with IndexedDB
// db/notesDB.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';
interface Note {
id: string;
title: string;
content: string;
timestamp: number;
synced: boolean;
}
interface NotesDB extends DBSchema {
notes: {
key: string;
value: Note;
indexes: { 'by-timestamp': number; 'by-synced': boolean };
};
}
let db: IDBPDatabase<NotesDB>;
export async function initDB() {
db = await openDB<NotesDB>('notes-db', 1, {
upgrade(db) {
const store = db.createObjectStore('notes', { keyPath: 'id' });
store.createIndex('by-timestamp', 'timestamp');
store.createIndex('by-synced', 'synced');
}
});
return db;
}
export async function addNote(note: Omit<Note, 'timestamp'>) {
return db.add('notes', {
...note,
timestamp: Date.now(),
synced: false
});
}
export async function getNotes(): Promise<Note[]> {
return db.getAll('notes');
}
export async function getUnsyncedNotes(): Promise<Note[]> {
return db.getAllFromIndex('notes', 'by-synced', false);
}
export async function updateNote(id: string, updates: Partial<Note>) {
const note = await db.get('notes', id);
if (note) {
await db.put('notes', { ...note, ...updates });
}
}
export async function markAsSynced(id: string) {
await updateNote(id, { synced: true });
}
5. Push Notifications
// services/pushNotification.ts
export async function subscribeToPushNotifications() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.log('Push notifications not supported');
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.REACT_APP_VAPID_PUBLIC_KEY
});
// Send subscription to server
await fetch('/api/push-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
} catch (error) {
console.error('Push subscription failed:', error);
}
}
// service-worker.ts
self.addEventListener('push', (event: PushEvent) => {
const data = event.data?.json() ?? {};
const options: NotificationOptions = {
title: data.title || 'New Notification',
body: data.message || '',
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
tag: data.tag || 'notification'
};
event.waitUntil(
self.registration.showNotification(options.title, options)
);
});
self.addEventListener('notificationclick', (event: NotificationEvent) => {
event.notification.close();
event.waitUntil(
self.clients.matchAll({ type: 'window' }).then(clients => {
if (clients.length > 0) {
return clients[0].focus();
}
return self.clients.openWindow('/');
})
);
});
Best Practices
- Implement service workers for offline support
- Create comprehensive web app manifest
- Use cache strategies appropriate for content type
- Provide offline fallback pages
- Test on various network conditions
- Optimize for slow 3G networks
- Include installation prompts
- Use IndexedDB for local storage
- Monitor sync status and connectivity
- Handle update notifications gracefully
Resources
Quick Install
/plugin add https://github.com/aj-geddes/useful-ai-prompts/tree/main/progressive-web-appCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
webapp-testing
TestingThis Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.
requesting-code-review
DesignThis skill dispatches a code-reviewer subagent to analyze code changes against requirements before proceeding. It should be used after completing tasks, implementing major features, or before merging to main. The review helps catch issues early by comparing the current implementation with the original plan.
