
Pwa Storefront
- 59 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Turn a store into an installable PWA with offline product browsing, push notifications, and home-screen access for mobile shoppers.
About
Implements a Workbox service worker, Web App Manifest, catalog caching, and order-update push to make a storefront installable and offline-capable. A developer uses it for shoppers on unreliable mobile connections or to enable Add to Home Screen without a native app.
- Workbox service worker caches the product catalog for offline browsing
- Web App Manifest enables Add to Home Screen re-engagement
Pwa Storefront by the numbers
- 59 all-time installs (skills.sh)
- Ranked #1,218 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill pwa-storefrontAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Turn a store into an installable PWA with offline product browsing, push notifications, and home-screen access for mobile shoppers.
Files
PWA Storefront
Overview
A Progressive Web App (PWA) storefront combines the reach of the web with native-app-like capabilities: offline catalog browsing, push notifications, home screen installation, and fast repeat loads from cache. Service workers intercept network requests and implement caching strategies that keep the store functional on flaky connections. This skill covers implementing a service worker with Workbox, creating a Web App Manifest, caching product catalogs, and sending push notifications for order updates.
When to Use This Skill
- When your customers are in regions with unreliable mobile internet connectivity
- When you want to enable "Add to Home Screen" for higher re-engagement rates without a native app
- When repeat page loads should be instant by serving assets from cache
- When you want to send push notifications for order status updates, back-in-stock alerts, or promotions
- When building a mobile-first storefront that needs to compete with native apps in UX quality
Prerequisites & Platform Notes
This skill is written for custom/headless storefronts (Node.js, Python, or similar backend). The code examples use TypeScript/Node.js and can be adapted to any stack.
Shopify: Shopify Hydrogen is Shopify's headless framework. MACH/composable patterns apply when using Shopify as the commerce backend with a custom frontend, or when mixing Shopify with other best-of-breed services. WooCommerce: WooCommerce can serve as a headless backend via its REST API and WPGraphQL. These patterns apply when decoupling the frontend from WordPress. Magento: Magento's GraphQL API and PWA Studio support headless architectures. These composable patterns apply to Magento as a backend service in a MACH stack.
You'll need:
- Node.js 18+ (or adapt to your backend language)
- PostgreSQL (or your preferred relational database)
- Redis for caching/queues
- An email sending service (SendGrid, AWS SES, or Postmark)
- CDN (Cloudflare, CloudFront, or Fastly)
Core Instructions
1. Create the Web App Manifest
The manifest makes the app installable on Android and iOS (iOS has partial support):
// public/manifest.json
{
"name": "My Commerce Store",
"short_name": "MyStore",
"description": "Fast, reliable shopping from anywhere",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1a1a2e",
"orientation": "portrait-primary",
"icons": [
{ "src": "/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" },
{ "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"screenshots": [
{ "src": "/screenshots/home.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" }
],
"categories": ["shopping"],
"share_target": {
"action": "/search",
"method": "GET",
"params": { "title": "q" }
}
}Link in your HTML:
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a1a2e">
<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-192x192.png">2. Register a service worker with Workbox
npm install workbox-webpack-plugin
# or for Vite:
npm install vite-plugin-pwaUsing vite-plugin-pwa (recommended for Vite/Next.js projects):
// vite.config.ts
import {VitePWA} from 'vite-plugin-pwa';
export default {
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,webp,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.googleapis\.com/,
handler: 'CacheFirst',
options: {cacheName: 'google-fonts-cache', expiration: {maxAgeSeconds: 60 * 60 * 24 * 365}},
},
{
urlPattern: /\/api\/products/,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'products-cache',
expiration: {maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}, // 24h
cacheableResponse: {statuses: [0, 200]},
},
},
{
urlPattern: /\/api\/collections/,
handler: 'NetworkFirst',
options: {
cacheName: 'collections-cache',
networkTimeoutSeconds: 3,
expiration: {maxEntries: 50, maxAgeSeconds: 60 * 60},
},
},
],
},
manifest: {/* inline manifest or path */},
}),
],
};3. Implement a custom service worker for offline catalog
For fine-grained control, write the service worker directly:
// public/sw.js
import {precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {StaleWhileRevalidate, CacheFirst, NetworkFirst} from 'workbox-strategies';
import {ExpirationPlugin} from 'workbox-expiration';
import {BackgroundSyncPlugin} from 'workbox-background-sync';
// Precache app shell (injected by build tool)
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// Product images: Cache-first with 7-day expiry
registerRoute(
({url}) => url.hostname.includes('cdn.shopify.com') || url.pathname.includes('/product-images/'),
new CacheFirst({
cacheName: 'product-images',
plugins: [
new ExpirationPlugin({maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 7}),
],
})
);
// Product API: Stale-while-revalidate (show cached, refresh in background)
registerRoute(
({url}) => url.pathname.startsWith('/api/products') || url.pathname.startsWith('/api/collections'),
new StaleWhileRevalidate({
cacheName: 'api-products',
plugins: [
new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 60 * 60 * 24}),
],
})
);
// Background sync for cart operations when offline
const cartSyncPlugin = new BackgroundSyncPlugin('cart-sync-queue', {
maxRetentionTime: 24 * 60, // Retry for 24 hours
});
registerRoute(
({url, request}) => url.pathname.startsWith('/api/cart') && request.method !== 'GET',
new NetworkFirst({plugins: [cartSyncPlugin]}),
'POST'
);4. Show an offline fallback page
// In the service worker
import {setCatchHandler, setDefaultHandler} from 'workbox-routing';
// Precache the offline page during installation
precacheAndRoute([{url: '/offline', revision: '1'}]);
// Serve offline page for navigation requests when network fails
setCatchHandler(async ({event}) => {
if (event.request.destination === 'document') {
return caches.match('/offline');
}
return Response.error();
}); // app/offline/page.tsx
export default function OfflinePage() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1>You're offline</h1>
<p>Check your connection. Recently viewed products are still available below.</p>
<RecentlyViewedProducts /> {/* Reads from IndexedDB */}
</div>
);
}5. Implement Web Push notifications
// client: subscribe to push notifications
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({subscription, customerId: user.id}),
});
}
// server: send push notification for order status update
import webpush from 'web-push';
webpush.setVapidDetails(
'mailto:support@mystore.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
export async function sendOrderUpdatePush(customerId: string, order: Order) {
const subscriptions = await db.pushSubscriptions.findByCustomer(customerId);
await Promise.allSettled(
subscriptions.map(sub =>
webpush.sendNotification(sub.data, JSON.stringify({
title: `Order #${order.number} Update`,
body: `Your order is now ${order.status}`,
icon: '/icons/icon-192x192.png',
url: `/orders/${order.id}`,
tag: `order-${order.id}`,
}))
)
);
}Handle push events in the service worker:
self.addEventListener('push', (event) => {
const data = event.data?.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
badge: '/icons/badge-72x72.png',
data: {url: data.url},
tag: data.tag,
renotify: true,
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});6. Detect and respond to offline status in the UI
// hooks/use-online-status.ts
import {useState, useEffect} from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true);
useEffect(() => {
const setOnline = () => setIsOnline(true);
const setOffline = () => setIsOnline(false);
window.addEventListener('online', setOnline);
window.addEventListener('offline', setOffline);
return () => { window.removeEventListener('online', setOnline); window.removeEventListener('offline', setOffline); };
}, []);
return isOnline;
}
// Usage in a component
function CartButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline} title={isOnline ? undefined : 'You are offline'}>
Add to Cart
</button>
);
}Examples
Lighthouse PWA audit checklist (automated)
# Install Lighthouse CLI
npm install -g lighthouse
# Audit PWA criteria
lighthouse https://mystore.com --preset=desktop --only-categories=pwa --output=json --output-path=./lighthouse-pwa.json
# Key scores to target:
# - "Installable" checks: manifest, service worker, HTTPS
# - "PWA Optimized" checks: themed address bar, offline page, mobile viewportIndexedDB catalog cache for offline browsing
import {openDB} from 'idb';
const db = await openDB('catalog-db', 1, {
upgrade(db) {
db.createObjectStore('products', {keyPath: 'id'});
db.createObjectStore('collections', {keyPath: 'id'});
},
});
// Store products when user browses online
export async function cacheProductsLocally(products: Product[]) {
const tx = db.transaction('products', 'readwrite');
await Promise.all([...products.map(p => tx.store.put(p)), tx.done]);
}
// Retrieve from IDB when offline
export async function getProductFromCache(id: string): Promise<Product | null> {
return db.get('products', id) ?? null;
}Best Practices
- Use `StaleWhileRevalidate` for product data — the user sees cached content immediately while the service worker fetches the latest data in the background
- Never cache cart or checkout pages — these must always be fresh; use
NetworkOnlystrategy for/cart,/checkout, and account pages - Version your service worker cache names — when you update your app, increment cache names so stale assets are purged automatically
- Test offline mode in Chrome DevTools — use the Network tab → "Offline" throttle to verify your offline experience before deploying
- Generate VAPID keys once and store them securely — VAPID private key loss means losing all existing push subscriptions; store in a secrets manager
- Request push permission with context — prompt users to allow notifications only after a relevant action (order placed, back-in-stock interested) to maximize opt-in rates
- Set reasonable cache size limits — use
ExpirationPluginwithmaxEntriesto prevent the service worker cache from consuming too much device storage
Common Pitfalls
| Problem | Solution |
|---|---|
| Service worker not updating after deployment | Use registerType: 'autoUpdate' and call skipWaiting() in the service worker to take control immediately; show a "New version available" toast |
| Push notifications not shown on iOS | iOS requires the user to add the PWA to the Home Screen first; Web Push on iOS Safari requires iOS 16.4+ and standalone display mode |
| Cached API responses served after price changes | Set maxAgeSeconds appropriately; use on-demand cache invalidation by busting cache names on deployment |
| Background sync fails silently | Wrap background sync in try/catch and log errors; test with the DevTools Application → Background Sync panel |
| App installability failing Lighthouse audit | Check for: HTTPS, valid manifest with 512×512 maskable icon, registered service worker, and start_url responding with 200 |
Related Skills
- @jamstack-storefront
- @image-optimization-cdn
- @edge-commerce
- @monitoring-alerting-commerce
{
"context": "Tests whether the agent correctly implements Web Push notifications using the web-push package, handles VAPID key security, uses the idb library for IndexedDB offline catalog, implements the useOnlineStatus hook correctly, and follows iOS push notification constraints.",
"type": "weighted_checklist",
"checklist": [
{
"name": "web-push package",
"max_score": 8,
"description": "The server-side push code imports from 'web-push' (not another push library)"
},
{
"name": "webpush.setVapidDetails",
"max_score": 8,
"description": "The server code calls webpush.setVapidDetails() with a mailto: address as the first argument, and VAPID public and private keys"
},
{
"name": "Promise.allSettled for bulk send",
"max_score": 10,
"description": "The code uses Promise.allSettled (NOT Promise.all) when sending notifications to multiple subscribers"
},
{
"name": "Push payload fields",
"max_score": 6,
"description": "The push notification payload includes title, body, icon, url, and tag fields"
},
{
"name": "SW badge and renotify",
"max_score": 8,
"description": "The service worker showNotification call includes badge set to an icon path and renotify: true"
},
{
"name": "notificationclick closes and opens",
"max_score": 6,
"description": "The notificationclick handler calls event.notification.close() AND clients.openWindow() with the notification's data URL"
},
{
"name": "userVisibleOnly subscription",
"max_score": 6,
"description": "The client-side pushManager.subscribe() call includes userVisibleOnly: true"
},
{
"name": "VAPID key as Uint8Array",
"max_score": 8,
"description": "The applicationServerKey in pushManager.subscribe() is converted from base64 string to Uint8Array (via a urlBase64ToUint8Array or equivalent conversion function)"
},
{
"name": "VAPID key storage warning",
"max_score": 8,
"description": "The PUSH_SETUP.md warns that the VAPID private key should be stored in a secrets manager and that losing it will invalidate all existing push subscriptions"
},
{
"name": "Permission timing advice",
"max_score": 6,
"description": "The PUSH_SETUP.md recommends requesting push permission only after a relevant user action (such as order placement or showing interest in back-in-stock alerts), not on page load"
},
{
"name": "iOS push limitations",
"max_score": 8,
"description": "The PUSH_SETUP.md mentions that iOS push requires iOS 16.4+ AND standalone display mode (user must add to Home Screen)"
},
{
"name": "idb library for IndexedDB",
"max_score": 6,
"description": "The offline page or related code uses the 'idb' library (openDB from 'idb') rather than raw IndexedDB API calls"
},
{
"name": "useOnlineStatus initial state",
"max_score": 6,
"description": "The useOnlineStatus hook initializes state with navigator.onLine (with a typeof navigator !== 'undefined' guard), NOT just hardcoded true"
},
{
"name": "Event listener cleanup",
"max_score": 6,
"description": "The useOnlineStatus hook's useEffect returns a cleanup function that removes both the 'online' and 'offline' event listeners"
}
]
}
Order Update Push Notifications and Offline Browsing
Problem/Feature Description
HomeFinds, a home goods retailer, wants to improve post-purchase engagement. Their data shows that customers who receive order status updates are significantly more likely to leave reviews and make repeat purchases. The team also wants to ensure that customers who lose their internet connection mid-browse can still see recently viewed products rather than hitting a dead end. They're building a Next.js storefront and need to add push notification support for order status changes, plus a polished offline experience.
The engineering team needs: a server-side API route that sends a push notification when an order status changes, a client-side subscription flow that requests permission at the right moment, and a service worker that handles displaying the notification and navigating to the right page when clicked. They also need the offline fallback page to show recently viewed products from local storage. The team is aware of iOS limitations and wants documentation on what customers will and won't experience on iOS devices.
A developer noted they once lost all push subscriptions during a server migration — the new implementation needs to address this risk explicitly.
Output Specification
Produce the following files:
app/api/push/send/route.ts— Next.js API route that accepts an order object and customer ID, and sends a push notification to all their subscribed deviceslib/push-client.ts— client-side code for subscribing to push notifications, including the subscription endpoint callpublic/sw-push.js— service worker event handlers for push events and notification clicksapp/offline/page.tsx— the offline fallback page component showing recently viewed products from IndexedDBhooks/use-online-status.ts— a React hook for tracking network connectivityPUSH_SETUP.md— documentation covering VAPID key generation, storage recommendations, iOS limitations, and when to prompt users for permission
The output should be complete, working TypeScript/JavaScript code.
{
"context": "Tests whether the agent applies the correct Workbox caching strategies for each resource type in a PWA storefront service worker, including specific configuration values, the prohibition on caching checkout/cart pages, and background sync for offline cart operations.",
"type": "weighted_checklist",
"checklist": [
{
"name": "vite-plugin-pwa used",
"max_score": 6,
"description": "The vite.config.ts uses vite-plugin-pwa (VitePWA import) rather than workbox-webpack-plugin or a manual service worker registration approach"
},
{
"name": "autoUpdate register type",
"max_score": 6,
"description": "The VitePWA plugin has registerType set to 'autoUpdate'"
},
{
"name": "Precache glob patterns",
"max_score": 6,
"description": "The Workbox globPatterns includes at least js, css, html, svg, png, webp, and woff2 file types"
},
{
"name": "precacheAndRoute + cleanupOutdatedCaches",
"max_score": 8,
"description": "The custom service worker (sw.js) calls both precacheAndRoute(self.__WB_MANIFEST) and cleanupOutdatedCaches()"
},
{
"name": "Product images CacheFirst",
"max_score": 8,
"description": "Product images (CDN hostname or /product-images/ path) use CacheFirst strategy"
},
{
"name": "Image cache expiry limits",
"max_score": 8,
"description": "Product image caching uses ExpirationPlugin with maxEntries of 200 and maxAgeSeconds of 604800 (7 days / 60*60*24*7)"
},
{
"name": "Products API StaleWhileRevalidate",
"max_score": 8,
"description": "The /api/products route uses StaleWhileRevalidate strategy"
},
{
"name": "Products cache limits",
"max_score": 10,
"description": "The products cache uses ExpirationPlugin with maxEntries of 500 and maxAgeSeconds of 86400 (24 hours)"
},
{
"name": "Collections NetworkFirst with timeout",
"max_score": 8,
"description": "The /api/collections route uses NetworkFirst strategy with networkTimeoutSeconds set to 3"
},
{
"name": "Cart background sync",
"max_score": 8,
"description": "Non-GET cart operations (/api/cart) use BackgroundSyncPlugin with a queue name and maxRetentionTime of 1440 (24 hours in minutes)"
},
{
"name": "No cart/checkout caching",
"max_score": 10,
"description": "The /cart and /checkout routes are NOT added to any caching strategy (no CacheFirst, StaleWhileRevalidate, or NetworkFirst for these paths) — or they are explicitly set to NetworkOnly"
},
{
"name": "ExpirationPlugin on all caches",
"max_score": 8,
"description": "Every runtime cache that is set up includes an ExpirationPlugin with maxEntries defined"
},
{
"name": "Cart uses NetworkFirst",
"max_score": 6,
"description": "The cart background sync route wraps the BackgroundSyncPlugin inside a NetworkFirst strategy (not CacheFirst or StaleWhileRevalidate)"
}
]
}
Offline-Ready Storefront Service Worker
Problem/Feature Description
OutdoorGear Co. runs an e-commerce store targeting hiking and camping enthusiasts — customers who frequently shop in areas with poor cell coverage (trailheads, campsites, rural sporting goods stores). Their Vite-based storefront currently has no offline capability, and the support team is getting complaints that the site breaks entirely when customers lose signal mid-browse. The engineering team has decided to add a service worker with proper caching to make product browsing work offline.
The product catalog is large (up to several hundred products) and served from /api/products, while product collections and categories are served from /api/collections. Product images are served from a CDN at cdn.outdoorgear.com and are typically stable for a week. The cart (/api/cart, /cart) and checkout (/checkout) flows must always reflect live data — stale cart or price information has caused customer service issues in the past. The engineering team also wants background synchronization so that cart modifications made offline are retried automatically once connectivity is restored.
Output Specification
Produce the following files:
vite.config.ts— Vite configuration that registers the PWA plugin with appropriate precaching settings and runtime caching rulespublic/sw.js— a custom service worker implementation using Workbox modules directly, covering product images, product/collections API, and cart background syncCACHING_STRATEGY.md— a brief document describing which strategy is used for each resource type and why, including what happens to cart operations when the user is offline
The output should be working code that an engineer can drop into the project with minimal modification.
{
"context": "Tests whether the agent correctly configures a Web App Manifest for an installable PWA storefront, including the specific field values, icon requirements, iOS HTML meta tags, and installability prerequisites.",
"type": "weighted_checklist",
"checklist": [
{
"name": "start_url with tracking param",
"max_score": 10,
"description": "The manifest's start_url is set to '/?source=pwa' (not just '/' or '/?')"
},
{
"name": "display standalone",
"max_score": 8,
"description": "The manifest's display field is set to 'standalone'"
},
{
"name": "Icon sizes present",
"max_score": 8,
"description": "The manifest includes icons at all three sizes: 72x72, 192x192, and 512x512"
},
{
"name": "Maskable icon purpose",
"max_score": 10,
"description": "The 192x192 and 512x512 icons have purpose set to 'any maskable' (not just 'any' or 'maskable' alone)"
},
{
"name": "Screenshots with form_factor",
"max_score": 8,
"description": "The manifest includes a screenshots array with at least one entry that has form_factor set to 'narrow'"
},
{
"name": "Shopping category",
"max_score": 6,
"description": "The manifest includes categories field containing 'shopping'"
},
{
"name": "Share target",
"max_score": 10,
"description": "The manifest includes a share_target with action '/search', method 'GET', and params mapping title to 'q'"
},
{
"name": "Apple meta tags",
"max_score": 10,
"description": "The HTML includes all three iOS tags: apple-mobile-web-app-capable (yes), apple-mobile-web-app-status-bar-style, and apple-touch-icon link"
},
{
"name": "Theme-color meta",
"max_score": 6,
"description": "The HTML includes a <meta name='theme-color'> tag"
},
{
"name": "Manifest link tag",
"max_score": 6,
"description": "The HTML includes <link rel='manifest' href='/manifest.json'>"
},
{
"name": "Installability requirements",
"max_score": 10,
"description": "The NOTES.md or output mentions at least 3 of these installability requirements: HTTPS, valid manifest with 512x512 maskable icon, registered service worker, start_url responding with 200"
},
{
"name": "No 72x72 maskable",
"max_score": 8,
"description": "The 72x72 icon does NOT have purpose 'maskable' — only the 192x192 and 512x512 do"
}
]
}
Mobile Storefront Launch Preparation
Problem/Feature Description
A fashion retailer, StyleNow, is preparing to launch their new online storefront and wants it to be installable on customers' home screens on both Android and iOS devices. Their marketing team has found that customers who install the app to their home screen have 3x higher re-engagement rates than those who only bookmark the site. The development lead has asked you to set up the foundational PWA configuration so the store can pass app store listing audits and show the "Add to Home Screen" prompt on Android.
The storefront is a Next.js app. The team has not yet created any manifest or service worker configuration — you are starting from scratch. They've told you the store name is "StyleNow", the short name is "StyleNow", the theme color is "#1a1a2e", background color is "#ffffff", and the primary language is English. They want the store to open at the root path and for users to be able to install it like a native app. The share functionality should allow sharing products to the store's search page.
Output Specification
Produce the following files:
public/manifest.json— the complete Web App Manifestapp/layout.tsx(orpages/_document.tsx) — an HTML layout or document file that links the manifest and includes the appropriate iOS compatibility meta tags and theme-color meta tagNOTES.md— a short explanation of the choices made (display mode, icon sizes, share target, etc.) and what a developer should verify before deploying to ensure the app is installable
Do not include actual image files — reference the icon paths as they would appear in production.
{
"name": "finsi/pwa-storefront",
"version": "0.1.0",
"summary": "Progressive web app storefronts with offline catalog, service workers",
"skills": {
"pwa-storefront": {
"path": "SKILL.md"
}
}
}