enhance-pwa — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited enhance-pwa (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A PWA closes the gap between "website" and "app". Users can install it to their home screen, it loads instantly from cache, and it keeps working when the network drops. This skill adds those capabilities to any existing web app without breaking what already works.
public/manifest.json or public/manifest.webmanifest → existing manifest
public/sw.js or src/sw.ts → existing service worker
vite.config.* → vite-plugin-pwa already configured?
next.config.* → next-pwa already configured?
package.json → workbox-*, vite-plugin-pwa, next-pwa, @vite-pwa/nuxtAlso check the current Lighthouse PWA score via:
// browser_evaluate after browser_navigate
const pwaReady = {
manifest: !!document.querySelector('link[rel="manifest"]'),
sw: 'serviceWorker' in navigator,
https: location.protocol === 'https:' || location.hostname === 'localhost',
};CallMcpTool(server: "user-firecrawl", toolName: "firecrawl_search", arguments: {
"query": "<framework> PWA service worker offline 2026 vite-plugin-pwa",
"limit": 3,
"sources": [{ "type": "web" }]
})Fetch the official docs for the relevant PWA plugin via Context7:
CallMcpTool(server: "plugin-context7-plugin-context7", toolName: "resolve-library-id", arguments: {
"libraryName": "vite-plugin-pwa"
})The manifest is what makes the app installable. Create or improve public/manifest.webmanifest:
{
"name": "Full App Name",
"short_name": "Short Name",
"description": "One sentence about what the app does",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#6366f1",
"orientation": "portrait-primary",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable any" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable any" }
],
"screenshots": [
{ "src": "/screenshots/home.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" },
{ "src": "/screenshots/home-mobile.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" }
]
}Reference the manifest in <head>:
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#6366f1" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />Icon requirements:
sharp or pwa-asset-generatorUse Workbox (via the framework plugin) rather than writing raw service worker code. Choose the right caching strategy per asset type:
| Asset type | Strategy | Why |
|---|---|---|
| App shell (HTML/JS/CSS) | CacheFirst + revision hash | Fast loads, updated on deploy |
| API data (list/detail) | NetworkFirst with 5s timeout | Fresh data, offline fallback |
| Images | CacheFirst, 30-day expiry | Rarely changes |
| Fonts | CacheFirst, 1-year expiry | Never changes |
| Analytics/telemetry | NetworkOnly | No value in caching |
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,webp}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/.*\/api\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxAgeSeconds: 60 * 60 * 24 },
networkTimeoutSeconds: 5,
},
},
],
},
manifest: { /* inline or separate file */ },
})Next.js 15+ has experimental PWA support. For stable Workbox integration:
npm install @ducanh2912/next-pwa// next.config.mjs
import withPWA from '@ducanh2912/next-pwa';
export default withPWA({
dest: 'public',
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
})({ /* rest of next config */ });If the app also ships as a Capacitor native app, the service worker must not intercept Capacitor's bridge requests. Add to the service worker:
// Do not cache Capacitor bridge calls
if (event.request.url.includes('capacitor://') ||
event.request.url.includes('ionic://')) {
return; // let it pass through
}When the network is unavailable and a cached response does not exist, show a helpful offline page rather than a browser error:
<!-- public/offline.html -->
<h1>You are offline</h1>
<p>Check your connection and try again. Pages you have visited recently will still load.</p>
<button onclick="location.reload()">Try again</button>Register it as the fallback in Workbox:
offlineFallback: true,
// or in workbox config:
navigationFallback: '/offline.html',Do not use the browser's default install prompt — it appears at the wrong time. Instead, intercept beforeinstallprompt and show it when the user has demonstrated value (e.g. after completing a core action):
// hooks/useInstallPrompt.ts
let deferredPrompt: BeforeInstallPromptEvent | null = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e as BeforeInstallPromptEvent;
});
export function triggerInstallPrompt() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
deferredPrompt.userChoice.then(() => { deferredPrompt = null; });
}Show a custom banner with clear benefits ("Install for offline access and faster loads"), not just "Add to Home Screen".
Only implement push if the app has a genuine reason to send notifications (not just to ask for permission on first load — users will deny that).
async function subscribeToPush(userId: string) {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_KEY!),
});
// Save subscription to backend
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify({ userId, subscription }),
});
}For the backend, use web-push (Node.js) or your platform's push service.
// Run Lighthouse via browser_evaluate
// (Playwright does not run Lighthouse natively — use Chrome DevTools Protocol)
const { lhr } = await page.evaluate(() => {
return new Promise((resolve) => {
// Trigger Lighthouse via CDP if available in the test environment
// Otherwise, use the Lighthouse CLI:
// npx lighthouse http://localhost:3000 --output json --output-path ./lh-report.json
});
});Or run Lighthouse via CLI and check the PWA category score:
npx lighthouse http://localhost:3000 \
--output json --output-path .playwright-mcp/lh-report.json \
--chrome-flags="--headless" 2>&1 | tail -5Target PWA score: ≥ 90. Key checks:
viewport meta tag presentstart_url loads while offlineAlways use registerType: 'autoUpdate' (Workbox prompts the user to reload when a new version is available) rather than silent background updates that serve stale code.
service worker by running mobile-emulator-test.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.