skills/progressive-enhancement-expert/SKILL.md
Build offline-first web apps with Service Workers, Workbox, background sync, and progressive enhancement strategies. Activate on: offline support, service worker caching, background sync, app shell, cache strategies, precaching. NOT for: native app packaging (use tauri-expert), general PWA manifest/install (use pwa-expert).
npx skillsauth add curiositech/windags-skills progressive-enhancement-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build resilient, offline-capable web applications using Service Workers, Workbox, background sync, and progressive enhancement -- apps that work without JavaScript and improve with it.
Activate on: offline-first architecture, service worker caching strategies, Workbox configuration, background sync for queued mutations, app shell pattern, cache-first vs. network-first decisions, precaching static assets.
NOT for: native desktop/mobile packaging -- use tauri-expert or React Native skills. PWA manifest, install prompts, and display modes -- use pwa-expert. General client-side caching with React Query -- use data-fetching-strategist.
| Domain | Technologies | Key Patterns |
|--------|-------------|--------------|
| Service Worker Toolkit | Workbox 7+, workbox-webpack-plugin, @serwist/next | Precaching, runtime caching, routing |
| Caching Strategies | CacheFirst, NetworkFirst, StaleWhileRevalidate | Per-route/resource strategy selection |
| Background Sync | Workbox Background Sync, SyncManager API | Queue failed requests, replay on reconnect |
| App Shell | Precached HTML/CSS/JS, navigation preload | Instant load, offline-capable shell |
| Offline UI | navigator.onLine, online/offline events | Connectivity banners, offline indicators |
| Progressive Forms | HTML forms with Server Actions, JS enhancement | Works without JS, enhanced with JS |
// next.config.ts
import withSerwistInit from '@serwist/next';
const withSerwist = withSerwistInit({
swSrc: 'app/sw.ts',
swDest: 'public/sw.js',
cacheOnNavigation: true,
reloadOnOnline: true,
});
export default withSerwist({
// ... rest of Next.js config
});
// app/sw.ts
import { defaultCache } from '@serwist/next/worker';
import { Serwist } from 'serwist';
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST, // auto-generated build manifest
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: [
...defaultCache,
// API responses: network-first with 24h cache fallback
{
urlPattern: /\/api\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 200, maxAgeSeconds: 86400 },
networkTimeoutSeconds: 3, // fall back to cache after 3s
},
},
// Images: cache-first (rarely change)
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/i,
handler: 'CacheFirst',
options: {
cacheName: 'image-cache',
expiration: { maxEntries: 100, maxAgeSeconds: 604800 }, // 7 days
},
},
],
});
serwist.addEventListeners();
// sw.ts -- register background sync queue
import { BackgroundSyncPlugin } from 'workbox-background-sync';
const bgSyncPlugin = new BackgroundSyncPlugin('mutation-queue', {
maxRetentionTime: 24 * 60, // retry for up to 24 hours
onSync: async ({ queue }) => {
let entry;
while ((entry = await queue.shiftRequest())) {
try {
await fetch(entry.request.clone());
} catch (error) {
await queue.unshiftRequest(entry); // put it back
throw error; // triggers retry
}
}
},
});
// Route POST/PUT/DELETE through the sync queue
registerRoute(
({ request }) => request.method !== 'GET' && request.url.includes('/api/'),
new NetworkOnly({ plugins: [bgSyncPlugin] }),
'POST'
);
// Client-side: queue-aware form submission
async function submitForm(data: FormData) {
try {
const res = await fetch('/api/submit', { method: 'POST', body: data });
if (!res.ok) throw new Error('Submit failed');
return { status: 'sent' };
} catch {
// Service Worker BackgroundSync will retry when online
return { status: 'queued' };
}
}
┌─ What are you caching? ────────────────────────────┐
│ │
│ Static build assets (JS/CSS bundles)? │
│ └─> Precache at install (immutable, hashed names) │
│ │
│ Images and fonts? │
│ └─> CacheFirst (rarely change, save bandwidth) │
│ │
│ API data (user content, listings)? │
│ └─> StaleWhileRevalidate (show cached, update bg) │
│ │
│ Auth tokens, real-time data? │
│ └─> NetworkOnly (never serve stale auth) │
│ │
│ HTML pages? │
│ └─> NetworkFirst with cache fallback │
│ (fresh content preferred, offline capable) │
└─────────────────────────────────────────────────────┘
activate event./offline fallback page.skipWaiting() without user notification -- silently activating a new SW mid-session can break in-progress work if the API contract changed. Show an "Update available" toast and let users choose when to refresh.navigator.onLine + event listeners)activate event<form action>)tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.