Comprehensive 185-point PWA audit beyond Lighthouse - analyzes manifest, service worker, offline capabilities, security, iOS compatibility, and advanced PWA features
Resources
5Install
npx skillscat add alexis315/pwa-review-skill Install via the SkillsCat registry.
PWA Review Skill
A comprehensive Progressive Web App audit that goes beyond standard Lighthouse testing. This skill analyzes PWAs across 11 categories with a 185-point scoring system, including advanced features and iOS-specific compatibility checks that typical audits miss.
Scoring Overview
| Category | Points | Focus |
|---|---|---|
| Manifest Compliance | 20 | Essential manifest fields |
| Advanced Manifest | 15 | Enhanced manifest features + iOS splash |
| Service Worker & Caching | 33 | SW implementation quality + caching strategies |
| Offline Capability | 19 | Offline functionality + storage + sync triggers |
| Installability | 13 | Install requirements |
| Security | 16 | Security measures |
| Performance Signals | 17 | Performance optimization + network detection |
| UX & Accessibility | 27 | User experience + iOS safe areas + mobile dropdowns + themes |
| SEO & Discoverability | 7 | Search optimization |
| PWA Advanced | 17 | Cutting-edge PWA features |
| iOS Compatibility | 1 | iOS-specific meta tags (bonus) |
Grading Scale: A+ (90%+), A (80-89%), B (70-79%), C (60-69%), D (40-59%), F (<40%)
Execution Workflow
When the user invokes /pwa-review <url>, follow these steps precisely:
Step 1: Fetch Target HTML
Use WebFetch to retrieve the target URL's HTML content.
WebFetch: {url}
Prompt: "Return the complete HTML source code. I need to analyze the head section for PWA-related tags including manifest link, meta tags, and inline scripts."Step 2: Extract PWA Resources
From the HTML, identify:
Manifest URL:
- Look for
<link rel="manifest" href="..."> - Convert relative URLs to absolute using the base URL
- If not found, note as CRITICAL issue
Service Worker Registration:
- Search for
navigator.serviceWorker.register('...')ornavigator.serviceWorker.register("...") - Extract the SW file path
- If not found, note as CRITICAL issue
Meta Tags to Extract:
<meta name="theme-color" content="..."><meta name="apple-mobile-web-app-capable" content="..."><meta name="apple-mobile-web-app-status-bar-style" content="..."><meta name="mobile-web-app-capable" content="..."><meta name="viewport" content="...">(check forviewport-fit=cover)<meta http-equiv="Content-Security-Policy" content="..."><link rel="apple-touch-icon" href="..."><link rel="apple-touch-startup-image" ...>(iOS splash screens)
Step 3: Fetch Manifest
If manifest URL was found, use WebFetch to retrieve it:
WebFetch: {manifest_url}
Prompt: "Return the complete manifest.json content as raw JSON."If manifest fetch fails (CORS, 404, etc.), score manifest categories as 0 and continue.
Step 4: Fetch Service Worker
If service worker URL was found, use WebFetch to retrieve it:
WebFetch: {sw_url}
Prompt: "Return the complete service worker JavaScript code."If SW fetch fails, score SW-related categories as 0 and continue.
Step 5: Analyze & Score
Evaluate each category using the detailed checklist below. Track:
- Passed items (full points)
- Failed items (0 points) - record as issues
- Partial items (partial points where applicable)
Step 6: Generate Report
Output a markdown report following the template at the end of this document.
Detailed Scoring Checklist
Category 1: Manifest Compliance (20 points)
| Check | Points | How to Verify |
|---|---|---|
name field present and non-empty |
2 | manifest.name exists and length > 0 |
short_name present (≤12 chars recommended) |
2 | manifest.short_name exists |
icons array with 192x192 PNG |
4 | icons array has item with sizes="192x192" |
icons array with 512x512 PNG |
4 | icons array has item with sizes="512x512" |
start_url defined |
2 | manifest.start_url exists |
display mode set (standalone/fullscreen/minimal-ui) |
2 | manifest.display is one of allowed values |
background_color specified |
2 | manifest.background_color exists (hex/rgb/named) |
theme_color specified |
2 | manifest.theme_color exists |
Critical Blocker: If manifest is missing entirely, this category scores 0/20.
Category 2: Advanced Manifest Features (15 points)
| Check | Points | How to Verify |
|---|---|---|
description field present |
1 | manifest.description exists |
screenshots array for install UI |
2 | manifest.screenshots array with ≥1 item |
shortcuts array for quick actions |
2 | manifest.shortcuts array with ≥1 item |
categories array defined |
1 | manifest.categories exists |
orientation preference set |
1 | manifest.orientation exists |
dir and lang for i18n |
1 | manifest.dir OR manifest.lang exists |
id field for app identity |
1 | manifest.id exists |
scope properly defined |
1 | manifest.scope exists |
| Maskable icon present | 1 | icons array has item with purpose="maskable" or "any maskable" |
note_taking object |
1 | manifest.note_taking exists (ChromeOS lock screen notes) |
widgets array |
1 | manifest.widgets exists (Windows 11 Widgets Board) |
| iOS splash screens present | 2 | <link rel="apple-touch-startup-image"> tags for multiple device sizes |
iOS Splash Screen Note: iOS requires separate <link rel="apple-touch-startup-image"> tags for each device size. Without these, iOS shows a blank white screen during PWA launch. Check for multiple media queries covering different device dimensions.
Category 3: Service Worker & Caching (33 points)
| Check | Points | How to Verify |
|---|---|---|
| SW registered in HTML | 2 | navigator.serviceWorker.register() found |
install event handler present |
3 | SW contains addEventListener('install', ...) or self.oninstall |
activate event handler present |
3 | SW contains addEventListener('activate', ...) or self.onactivate |
fetch event handler present |
4 | SW contains addEventListener('fetch', ...) or self.onfetch |
| Cache API usage (caches.open/put/match) | 3 | SW contains caches.open or cache.put or cache.match |
| Cache versioning/naming strategy | 2 | SW has cache name variable (CACHE_NAME, CACHE_VERSION, etc.) |
| Old cache cleanup in activate | 2 | activate handler deletes old caches |
| Background Sync support | 2 | SW contains addEventListener('sync', ...) or addEventListener('periodicsync', ...) |
| Workbox usage (bonus, not required) | 1 | SW imports workbox or uses workbox.* methods |
skipWaiting() usage |
1 | SW contains self.skipWaiting() for instant activation |
clients.claim() usage |
1 | SW contains clients.claim() for immediate control |
| Navigation preload | 1 | SW uses navigationPreload.enable() |
| Stale-while-revalidate pattern | 1 | fetch handler serves cache then updates in background |
| Push event handler | 1 | SW contains addEventListener('push', ...) |
| notificationclick handler | 1 | SW contains addEventListener('notificationclick', ...) |
| Notification action buttons | 1 | Push notifications include actions array OR notificationclick checks event.action |
| Multiple caching strategies | 2 | SW uses different strategies for different routes (CacheFirst, NetworkFirst, StaleWhileRevalidate) |
| Cache expiration config | 1 | SW has maxEntries or maxAgeSeconds for cache pruning |
| SW message handler | 1 | SW contains addEventListener('message', ...) for client communication |
Critical Blocker: If no service worker, this category scores 0/33.
Caching Strategies Note: Production-grade PWAs should use different caching strategies based on resource type:
- CacheFirst: Static assets, fonts, images (rarely change)
- NetworkFirst: API responses, dynamic content (freshness matters)
- StaleWhileRevalidate: JS/CSS bundles (speed + freshness balance)
Look for patterns likenew CacheFirst(),new NetworkFirst(), or explicit strategy patterns in fetch handlers.
Cache Expiration Note: Without expiration limits, caches grow unbounded and can exceed storage quotas. Look for ExpirationPlugin with maxEntries or maxAgeSeconds, or custom cleanup logic in the fetch handler.
Category 4: Offline Capability (19 points)
| Check | Points | How to Verify |
|---|---|---|
| Offline fallback page defined | 3 | SW caches and serves an offline.html or similar |
| App shell resources precached | 3 | install event caches core HTML/CSS/JS files |
| Offline indicator in UI (code pattern) | 2 | Code checks navigator.onLine or listens to online/offline events |
| Network-first or cache-first strategy evident | 2 | fetch handler has clear strategy pattern |
| Update prompt shown to user | 1 | Code handles SW update with user notification (e.g., "New version available") |
| Graceful update flow | 1 | Update doesn't force reload without warning, user can choose when to update |
| Update state persistence | 1 | localStorage flag prevents update prompt re-appearing after update (e.g., pwa-just-updated) |
| Touch event double-fire prevention | 1 | Update/action handlers prevent duplicate execution from onClick + onTouchEnd |
| Persistent storage request | 1 | Code uses navigator.storage.persist() to prevent iOS data eviction |
| IndexedDB offline storage | 1 | Code uses indexedDB.open() or idb library for structured offline data |
| Storage quota monitoring | 1 | Code uses navigator.storage.estimate() for storage health checks |
| Background sync client trigger | 1 | Client triggers registration.sync.register() when coming back online |
| Periodic sync registration | 1 | Client registers registration.periodicSync.register() on app init |
Update UX Note: Good PWAs notify users when updates are available and let them choose when to apply the update. Look for patterns like useRegisterSW, workbox-window, or custom SW update handling with user-facing notifications.
Background Sync Client Trigger Note: Having a sync event handler in the service worker is not enough. The client must explicitly trigger background sync when coming back online by calling navigator.serviceWorker.ready.then(reg => reg.sync.register('sync-pending-requests')) in the online event listener. Without this, offline requests remain queued indefinitely.
Periodic Sync Registration Note: The service worker periodicsync event handler must be complemented by client-side registration during app initialization. Look for registration.periodicSync.register('sync-content', { minInterval: ... }) wrapped in a permission check (navigator.permissions.query({ name: 'periodic-background-sync' })). This enables automatic background content updates even when the app is closed.
Update State Note: After a user clicks "Update", the PWA reloads. Without state management, the update prompt may immediately re-appear because the new service worker is still "waiting". Use localStorage flags (e.g., pwa-just-updated with timestamp) to suppress the prompt for a brief period (30 seconds) after update completion. Also implement double-fire prevention for touch handlers - on iOS, both onClick and onTouchEnd may fire, causing duplicate updates.
Offline Storage Note: For complex PWAs with user-generated content, localStorage alone is insufficient. Use IndexedDB for structured data storage (images, generation history, preferences). Request persistent storage with navigator.storage.persist() to prevent iOS from evicting data after 7 days of inactivity. Monitor storage quota with navigator.storage.estimate() to warn users before running out of space.
Category 5: Installability Requirements (13 points)
| Check | Points | How to Verify |
|---|---|---|
| Served over HTTPS | 3 | URL starts with https:// |
| Valid manifest linked in HTML | 2 | exists with valid href |
| Service worker with fetch handler | 2 | Covered in SW category, cross-check |
| 192x192 icon present | 1 | Covered in manifest, cross-check |
| 512x512 icon present | 1 | Covered in manifest, cross-check |
apple-touch-icon for iOS |
1 | in HTML |
beforeinstallprompt handled |
2 | HTML/JS contains beforeinstallprompt event listener |
| Custom install UI | 1 | Code shows/hides custom install button |
Note: prefer_related_applications: true in manifest BLOCKS browser install prompt - flag as CRITICAL if found.
Category 6: Security Measures (16 points)
| Check | Points | How to Verify |
|---|---|---|
| HTTPS enforced | 2 | URL is https:// (duplicate check for emphasis) |
| Content Security Policy present | 3 | CSP meta tag or mention in SW/HTML |
| Subresource Integrity (SRI) on scripts | 2 | <script> tags have integrity="sha..."</td>
</tr>
<tr>
<td>No mixed content</td>
<td>2</td>
<td>No http:// resources loaded on https:// page</td>
</tr>
<tr>
<td>scope restricted appropriately</td>
<td>1</td>
<td>manifest.scope doesn't expose unnecessary paths</td>
</tr>
<tr>
<td>Cross-Origin Isolation (COOP/COEP)</td>
<td>2</td>
<td>Headers: Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy</td>
</tr>
<tr>
<td>HSTS header</td>
<td>1</td>
<td>Strict-Transport-Security header (note: not detectable from HTML)</td>
</tr>
<tr>
<td>X-Content-Type-Options</td>
<td>1</td>
<td>nosniff header present (note: not detectable from HTML)</td>
</tr>
<tr>
<td>Referrer-Policy</td>
<td>1</td>
<td>Appropriate referrer policy set via meta or header</td>
</tr>
<tr>
<td>Permissions-Policy</td>
<td>1</td>
<td>Feature policy defined (note: not detectable from HTML)</td>
</tr>
</tbody></table>
<p><strong>Note:</strong> Some security headers (HSTS, X-Content-Type-Options, Permissions-Policy) cannot be verified from HTML alone. Mark as "Unable to verify" unless response headers are available.</p>
<h3>Category 7: Performance Signals (17 points)</h3>
<table>
<thead>
<tr>
<th>Check</th>
<th>Points</th>
<th>How to Verify</th>
</tr>
</thead>
<tbody><tr>
<td>No render-blocking scripts in head</td>
<td>2</td>
<td>Scripts have defer/async or are at body end</td>
</tr>
<tr>
<td>Images have lazy loading</td>
<td>2</td>
<td><img loading="lazy"> or Intersection Observer usage</td>
</tr>
<tr>
<td>Resource hints present</td>
<td>2</td>
<td><link rel="preload/prefetch/preconnect"> found</td>
</tr>
<tr>
<td>Code splitting indicators</td>
<td>2</td>
<td>Multiple JS chunks or dynamic import() usage</td>
</tr>
<tr>
<td>Font optimization</td>
<td>2</td>
<td>font-display: swap or preloaded fonts</td>
</tr>
<tr>
<td>LCP optimization signals</td>
<td>1</td>
<td>Hero image preloaded, above-fold content prioritized</td>
</tr>
<tr>
<td>INP optimization signals</td>
<td>1</td>
<td>No long tasks, event handlers optimized (qualitative)</td>
</tr>
<tr>
<td>CLS prevention</td>
<td>1</td>
<td>Images have width/height, no layout shifts expected</td>
</tr>
<tr>
<td>Critical CSS inlined</td>
<td>1</td>
<td>Critical styles in <head> or preloaded</td>
</tr>
<tr>
<td>Compression headers</td>
<td>1</td>
<td>Server returns <code>Content-Encoding: gzip</code> or <code>br</code> (note: verify via DevTools)</td>
</tr>
<tr>
<td>Bundle chunking strategy</td>
<td>1</td>
<td>Build uses <code>manualChunks</code>, vendor splitting, or separate runtime chunks</td>
</tr>
<tr>
<td>Network Information API usage</td>
<td>1</td>
<td>Code uses <code>navigator.connection</code> for adaptive behavior on slow networks</td>
</tr>
</tbody></table>
<p><strong>Compression Note:</strong> Gzip/Brotli compression can reduce bundle sizes by 60-80%. This cannot be verified from HTML alone - check Network tab in DevTools for <code>Content-Encoding</code> response header. Build tools like <code>vite-plugin-compression</code> can generate pre-compressed <code>.gz</code> and <code>.br</code> files.</p>
<p><strong>Bundle Chunking Note:</strong> Good build configurations split vendor dependencies (React, UI libraries, i18n) into separate chunks. Look for patterns like <code>manualChunks</code> in Vite/Rollup config or webpack's <code>splitChunks</code>. This enables better caching (vendor chunks change less frequently) and parallel loading.</p>
<p><strong>Network Information API Note:</strong> The Network Information API (<code>navigator.connection</code>) enables adaptive behavior based on connection quality. Look for patterns that check <code>connection.effectiveType</code> (4g/3g/2g/slow-2g), <code>connection.saveData</code>, or <code>connection.downlink</code>. PWAs can reduce image quality, disable prefetching, or extend API timeouts on slow connections. Example:</p>
<pre><code class="language-javascript" data-language="javascript">const conn = navigator.connection;
if (conn?.effectiveType === '2g' || conn?.saveData) {
// Load low-quality images, disable autoplay, extend timeouts
}</code></pre><h3>Category 8: UX & Accessibility (27 points)</h3>
<table>
<thead>
<tr>
<th>Check</th>
<th>Points</th>
<th>How to Verify</th>
</tr>
</thead>
<tbody><tr>
<td>Responsive viewport meta</td>
<td>2</td>
<td><meta name="viewport" content="width=device-width, initial-scale=1"></td>
</tr>
<tr>
<td><code>viewport-fit=cover</code> for safe areas</td>
<td>2</td>
<td>Viewport meta includes <code>viewport-fit=cover</code> (required for iOS notch/Dynamic Island)</td>
</tr>
<tr>
<td>Safe area CSS usage</td>
<td>2</td>
<td>Code uses <code>env(safe-area-inset-*)</code> for fixed/sticky elements</td>
</tr>
<tr>
<td>Semantic HTML structure</td>
<td>2</td>
<td><main>, <nav>, <header>, <footer> tags present</td>
</tr>
<tr>
<td>ARIA landmarks or roles</td>
<td>2</td>
<td>role="..." or aria-* attributes found</td>
</tr>
<tr>
<td>Language declared</td>
<td>2</td>
<td><html lang="..."> attribute present</td>
</tr>
<tr>
<td>Touch-friendly targets</td>
<td>2</td>
<td>No evidence of tiny click targets (qualitative)</td>
</tr>
<tr>
<td>Touch event handling for iOS</td>
<td>1</td>
<td>Critical buttons have <code>onTouchEnd</code> handlers or <code>touch-manipulation</code> CSS</td>
</tr>
<tr>
<td>Focus indicators visible</td>
<td>1</td>
<td>:focus styles not removed, visible outlines (qualitative)</td>
</tr>
<tr>
<td>Skip to main content link</td>
<td>1</td>
<td>Skip link present for keyboard navigation</td>
</tr>
<tr>
<td>Mobile dropdown positioning</td>
<td>2</td>
<td>Dropdowns use <code>fixed</code> on mobile, <code>absolute</code> on desktop with proper margins</td>
</tr>
<tr>
<td>Dropdown safe area handling</td>
<td>1</td>
<td>Dropdowns apply <code>safe-area-inset-right/left</code> for notch devices</td>
</tr>
<tr>
<td>Theme consistency (light/dark)</td>
<td>2</td>
<td>All UI elements have both light and <code>dark:</code> variants in Tailwind/CSS</td>
</tr>
<tr>
<td>Dark overlay theme pairs</td>
<td>1</td>
<td><code>bg-black/X</code> patterns have <code>dark:</code> prefix (e.g., <code>bg-white/60 dark:bg-black/60</code>)</td>
</tr>
<tr>
<td>Border visibility pairs</td>
<td>1</td>
<td><code>border-white/X</code> has light alternative (e.g., <code>border-zinc-200 dark:border-white/10</code>)</td>
</tr>
<tr>
<td>Hover state theme pairs</td>
<td>1</td>
<td>Hover backgrounds have both variants (e.g., <code>hover:bg-zinc-100 dark:hover:bg-white/10</code>)</td>
</tr>
<tr>
<td>Gradient theme support</td>
<td>1</td>
<td>Gradient stops have variants (e.g., <code>from-white/80 dark:from-black/80</code>)</td>
</tr>
<tr>
<td>Contextual text-white</td>
<td>1</td>
<td>White text only on colored backgrounds, not transparent overlays</td>
</tr>
</tbody></table>
<p><strong>iOS Safe Area Note:</strong> iPhone notch and Dynamic Island require special handling. Without <code>viewport-fit=cover</code> and <code>env(safe-area-inset-*)</code> CSS, content may be obscured or buttons may be unreachable in PWA standalone mode. Fixed headers should use <code>padding-top: env(safe-area-inset-top)</code> and bottom navigation should account for <code>safe-area-inset-bottom</code>.</p>
<p><strong>Touch Event Note:</strong> On iOS, <code>onClick</code> handlers may not fire reliably in PWA mode. Critical action buttons (update, install, submit) should include <code>onTouchEnd</code> handlers as backup. The CSS property <code>touch-manipulation</code> prevents double-tap zoom delays.</p>
<p><strong>Mobile Dropdown Positioning Note:</strong> Dropdowns positioned with <code>absolute</code> relative to a small parent element (like a button) often extend beyond the viewport on mobile. Solution: Use <code>fixed</code> positioning on mobile to break out of the parent's positioning context, then use <code>left-4 right-4</code> (or similar) for consistent margins instead of transform centering (<code>left-1/2 -translate-x-1/2</code>). On desktop (<code>sm:</code> breakpoint), revert to <code>absolute</code> with <code>right-0</code> for proper alignment. Always apply <code>safe-area-inset-right</code> via inline style for notch devices.</p>
<p><strong>Theme Consistency Note:</strong> In Tailwind CSS projects, all UI elements should have both light and dark variants. Look for patterns like <code>bg-zinc-100 dark:bg-zinc-900</code>. Hardcoded colors without a <code>dark:</code> counterpart (e.g., <code>bg-zinc-900</code> alone) will appear incorrectly in light mode. Common problem areas: tooltips, buttons, borders, and dropdown backgrounds.</p>
<p><strong>Extended Theme Checks Note:</strong> Alpha/opacity-based colors (<code>bg-black/60</code>, <code>border-white/10</code>, <code>from-black/80</code>) are commonly used for dark mode but invisible or wrong in light mode. Each pattern needs a light mode counterpart:</p>
<ul>
<li><code>bg-black/60</code> → <code>bg-white/60 dark:bg-black/60</code></li>
<li><code>border-white/10</code> → <code>border-zinc-200 dark:border-white/10</code></li>
<li><code>hover:bg-black/10</code> → <code>hover:bg-zinc-100 dark:hover:bg-white/10</code></li>
<li><code>from-black/80</code> → <code>from-white/80 dark:from-black/80</code></li>
<li><code>text-white</code> on overlays → <code>text-zinc-900 dark:text-white</code><br>These patterns are frequently missed because they work in dark mode (the default design) but break in light mode.</li>
</ul>
<h3>Category 9: SEO & Discoverability (7 points)</h3>
<table>
<thead>
<tr>
<th>Check</th>
<th>Points</th>
<th>How to Verify</th>
</tr>
</thead>
<tbody><tr>
<td><code><title></code> tag present</td>
<td>1</td>
<td>HTML has <title> with content</td>
</tr>
<tr>
<td>Meta description</td>
<td>2</td>
<td><meta name="description" content="..."></td>
</tr>
<tr>
<td>Open Graph tags</td>
<td>2</td>
<td>og:title, og:description, og:image present</td>
</tr>
<tr>
<td>Canonical URL</td>
<td>1</td>
<td><link rel="canonical" href="..."></td>
</tr>
<tr>
<td>Structured data (JSON-LD)</td>
<td>1</td>
<td><script type="application/ld+json"> found</td>
</tr>
</tbody></table>
<h3>Category 10: PWA Advanced Capabilities (17 points)</h3>
<table>
<thead>
<tr>
<th>Check</th>
<th>Points</th>
<th>How to Verify</th>
</tr>
</thead>
<tbody><tr>
<td><code>handle_links</code> preference</td>
<td>2</td>
<td>manifest.handle_links exists (preferred/auto/not-preferred)</td>
</tr>
<tr>
<td><code>launch_handler</code> defined</td>
<td>2</td>
<td>manifest.launch_handler object exists</td>
</tr>
<tr>
<td><code>file_handlers</code> array</td>
<td>2</td>
<td>manifest.file_handlers with accept types</td>
</tr>
<tr>
<td><code>protocol_handlers</code> array</td>
<td>2</td>
<td>manifest.protocol_handlers for custom protocols</td>
</tr>
<tr>
<td><code>share_target</code> defined</td>
<td>2</td>
<td>manifest.share_target object exists</td>
</tr>
<tr>
<td><code>display_override</code> array</td>
<td>1</td>
<td>manifest.display_override for fallback displays</td>
</tr>
<tr>
<td><code>edge_side_panel</code> for Edge</td>
<td>1</td>
<td>manifest.edge_side_panel object exists</td>
</tr>
<tr>
<td><code>scope_extensions</code></td>
<td>1</td>
<td>manifest.scope_extensions array exists</td>
</tr>
<tr>
<td><code>related_applications</code> (informational)</td>
<td>1</td>
<td>manifest.related_applications exists</td>
</tr>
<tr>
<td><code>prefer_related_applications</code> is false/absent</td>
<td>1</td>
<td>Value is false or field is missing (true = CRITICAL issue)</td>
</tr>
<tr>
<td>Web Push configured</td>
<td>1</td>
<td>VAPID or gcm_sender_id in manifest</td>
</tr>
<tr>
<td>Notification permission UX</td>
<td>1</td>
<td>Permission requested after user action, not on load</td>
</tr>
</tbody></table>
<h3>Category 11: iOS Compatibility Bonus (1 point)</h3>
<table>
<thead>
<tr>
<th>Check</th>
<th>Points</th>
<th>How to Verify</th>
</tr>
</thead>
<tbody><tr>
<td>Complete iOS meta tag set</td>
<td>1</td>
<td>Has <code>apple-mobile-web-app-capable</code>, <code>apple-mobile-web-app-status-bar-style</code>, AND <code>mobile-web-app-capable</code></td>
</tr>
</tbody></table>
<p><strong>Note:</strong> This is a bonus point for PWAs that have complete iOS compatibility meta tags. The individual checks are scored in their respective categories, but having the complete set demonstrates attention to cross-platform compatibility.</p>
<hr>
<h2>Issue Classification</h2>
<h3>Critical Issues (Must Fix)</h3>
<ul>
<li>Missing manifest file</li>
<li>Missing service worker</li>
<li>No fetch event handler in SW</li>
<li><code>prefer_related_applications: true</code> (blocks install)</li>
<li>Not served over HTTPS</li>
<li>Missing required icons (192x192, 512x512)</li>
</ul>
<h3>Warnings (Should Fix)</h3>
<ul>
<li>Missing theme_color/background_color</li>
<li>No offline fallback page</li>
<li>No CSP header/meta</li>
<li>Missing apple-touch-icon</li>
<li>No cache versioning strategy</li>
<li>Missing meta description</li>
<li>No skipWaiting/clients.claim</li>
<li>No beforeinstallprompt handling</li>
<li>Missing <code>viewport-fit=cover</code> (iOS safe areas won't work)</li>
<li>No <code>env(safe-area-inset-*)</code> usage for fixed elements</li>
<li>Missing iOS splash screens</li>
<li>No update notification UX for users</li>
<li>No update state persistence (prompt may re-appear after update)</li>
<li>Dropdowns use absolute positioning without mobile viewport handling</li>
<li>Hardcoded colors without light/dark theme variants</li>
<li><code>bg-black/X</code> patterns without <code>dark:</code> prefix (invisible in light mode)</li>
<li><code>border-white/X</code> patterns without light alternative (invisible in light mode)</li>
<li><code>hover:bg-black/X</code> or <code>hover:bg-white/X</code> without theme pair</li>
<li>Gradient stops without theme variants</li>
<li><code>text-white</code> on transparent/overlay backgrounds (unreadable in light mode)</li>
</ul>
<h3>Informational (Nice to Have)</h3>
<ul>
<li>Missing advanced manifest features</li>
<li>No PWA advanced capabilities</li>
<li>No structured data</li>
<li>Missing shortcuts</li>
<li>No navigation preload</li>
<li>No stale-while-revalidate</li>
<li>No persistent storage request (iOS data may be evicted)</li>
<li>No IndexedDB usage (limited to localStorage)</li>
<li>No storage quota monitoring</li>
<li>No compression headers detected</li>
<li>No bundle chunking strategy evident</li>
<li>No background sync client trigger (offline requests never sync)</li>
<li>No periodic sync registration (no background updates)</li>
<li>No Network Information API usage (no adaptive behavior on slow networks)</li>
<li>Single caching strategy for all resources (not optimized)</li>
<li>No cache expiration config (unbounded cache growth)</li>
</ul>
<hr>
<h2>Report Template</h2>
<p>Generate the report in this exact format:</p>
<pre><code class="language-markdown" data-language="markdown"># PWA Audit Report
**URL:** [analyzed URL]
**Date:** [current date]
**Overall Score:** [X]/185 ([percentage]%) — Grade: [letter grade]
---
## Score Breakdown
| Category | Score | Status |
|----------|-------|--------|
| Manifest Compliance | X/20 | [status emoji] |
| Advanced Manifest | X/15 | [status emoji] |
| Service Worker & Caching | X/33 | [status emoji] |
| Offline Capability | X/19 | [status emoji] |
| Installability | X/13 | [status emoji] |
| Security | X/16 | [status emoji] |
| Performance Signals | X/17 | [status emoji] |
| UX & Accessibility | X/27 | [status emoji] |
| SEO & Discoverability | X/7 | [status emoji] |
| PWA Advanced | X/17 | [status emoji] |
| iOS Compatibility | X/1 | [status emoji] |
Status: Pass (80%+), Warn (50-79%), Fail (<50%)
---
## Critical Issues
[List any critical blockers that prevent PWA functionality]
---
## Warnings
[List important issues that should be addressed]
---
## Passed Checks
[Summarize what the PWA does well]
---
## Recommendations
### High Priority
1. [Most impactful fix]
2. [Second priority]
### Medium Priority
1. [Improvement]
2. [Enhancement]
### Quick Wins
- [Easy fix 1]
- [Easy fix 2]
---
## Resources
- [Web App Manifest | web.dev](https://web.dev/add-manifest/)
- [Service Workers | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
- [PWA Checklist | web.dev](https://web.dev/pwa-checklist/)
- [Workbox | Google](https://developer.chrome.com/docs/workbox/)
---
*Generated by PWA Review Skill v5.4.0*</code></pre><hr>
<h2>Error Handling</h2>
<h3>Manifest Not Found</h3>
<ul>
<li>Score Category 1 (Manifest Compliance) as 0/20</li>
<li>Score Category 2 (Advanced Manifest) as 0/13</li>
<li>Add CRITICAL issue: "No manifest.json found"</li>
<li>Continue with remaining categories</li>
</ul>
<h3>Service Worker Not Found</h3>
<ul>
<li>Score Category 3 (Service Worker & Caching) as 0/33</li>
<li>Score Category 4 (Offline Capability) as 0/19</li>
<li>Reduce Category 5 (Installability) by 2 points</li>
<li>Add CRITICAL issue: "No service worker registered"</li>
<li>Continue with remaining categories</li>
</ul>
<h3>CORS/Fetch Failures</h3>
<ul>
<li>Note which resource couldn't be fetched</li>
<li>Score affected categories as 0</li>
<li>Add WARNING: "Could not fetch [resource] - CORS or access issue"</li>
<li>Analyze whatever resources were successfully retrieved</li>
</ul>
<h3>Invalid JSON (Manifest)</h3>
<ul>
<li>Score manifest categories as 0</li>
<li>Add CRITICAL issue: "manifest.json contains invalid JSON"</li>
<li>Continue with HTML and SW analysis</li>
</ul>
<hr>
<h2>iOS/Safari Limitations to Note</h2>
<p>When generating the report, include these platform-specific notes if relevant:</p>
<h3>Installation & Capabilities</h3>
<ul>
<li>iOS Safari: <code>beforeinstallprompt</code> event not supported (users must manually "Add to Home Screen")</li>
<li>iOS Safari: Push notifications require iOS 16.4+ and explicit user permission</li>
<li>iOS Safari: Storage limited to ~50MB (may be evicted under storage pressure)</li>
<li>iOS Safari: No persistent storage API</li>
<li>Safari: Service worker scope limitations more strict</li>
</ul>
<h3>Safe Area & Display (Critical for PWA Mode)</h3>
<ul>
<li><strong>Notch/Dynamic Island</strong>: Without <code>viewport-fit=cover</code> in viewport meta, <code>env(safe-area-inset-*)</code> won't work</li>
<li><strong>Fixed Headers</strong>: Must use <code>padding-top: env(safe-area-inset-top)</code> to avoid content being hidden behind notch</li>
<li><strong>Fixed Bottom Elements</strong>: Must use <code>padding-bottom: env(safe-area-inset-bottom)</code> for home indicator area</li>
<li><strong>Status Bar</strong>: <code>apple-mobile-web-app-status-bar-style</code> can be <code>default</code>, <code>black</code>, or <code>black-translucent</code></li>
<li>PWA mode on iOS shows no browser chrome - safe area handling is essential</li>
</ul>
<h3>Touch Events & Interactions</h3>
<ul>
<li><code>onClick</code> handlers may not fire reliably on some iOS versions in PWA mode</li>
<li>Add <code>onTouchEnd</code> as backup for critical buttons (install, update, submit actions)</li>
<li>Use <code>touch-manipulation</code> CSS to eliminate 300ms tap delay and prevent double-tap zoom</li>
<li>Use <code>cursor: pointer</code> CSS on interactive elements - iOS Safari requires this to recognize elements as clickable</li>
<li>Use <code>-webkit-tap-highlight-color: transparent</code> for clean visual feedback</li>
<li>Use <code>-webkit-user-select: none</code> on interactive elements to prevent text selection</li>
</ul>
<h3>Splash Screens</h3>
<ul>
<li>iOS requires <code><link rel="apple-touch-startup-image"></code> with media queries for each device size</li>
<li>Without splash screens, iOS shows blank white screen during PWA launch</li>
<li>Each iPhone/iPad dimension needs its own splash image (portrait and landscape)</li>
</ul>
<h3>Z-Index & Stacking Context (Critical)</h3>
<ul>
<li><strong>backdrop-filter creates new stacking context</strong>: Headers with <code>backdrop-blur</code> or <code>backdrop-filter</code> create isolated stacking contexts in iOS Safari. Elements with higher z-index values may still appear BEHIND these elements.</li>
<li><strong>Fix</strong>: Add <code>transform: translate3d(0,0,0)</code> to elements that need to appear above backdrop-filter elements. This forces GPU layer rendering and fixes stacking order.</li>
<li>Toast/notification components must have high z-index (e.g., <code>z-[9999]</code>) AND <code>transform: translate3d(0,0,0)</code> to appear above blurred headers</li>
<li>iOS Safari has stricter stacking context behavior than Chrome/Firefox</li>
</ul>
<p><strong>Example fix for notifications above blurred headers:</strong></p>
<pre><code class="language-css" data-language="css">.notification {
position: fixed;
z-index: 9999;
transform: translate3d(0,0,0); /* Forces GPU layer, fixes iOS stacking */
}</code></pre><hr>
<h2>Example Usage</h2>
<p>User: <code>/pwa-review https://looknex.com</code></p>
<p>Claude will:</p>
<ol>
<li>Fetch <a href="https://looknex.com" target="_blank" rel="noopener noreferrer nofollow">https://looknex.com</a> HTML</li>
<li>Find manifest at /manifest.json</li>
<li>Find SW at /sw.js</li>
<li>Fetch and analyze both files</li>
<li>Score across all 10 categories</li>
<li>Generate detailed report with findings</li>
</ol>
</pwa-review>
Categories |