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>)data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.