Recipe: podcast PWA on Workbox
The podcast guide uses offlibrarySW(). This is the same app on Workbox: precached shell, runtime caching for artwork, and episodes served by Offlibrary.
Service worker
Section titled “Service worker”// src/sw.js (built with injectManifest)import { clientsClaim } from 'workbox-core';import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';import { registerRoute, NavigationRoute } from 'workbox-routing';import { CacheFirst, NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies';import { ExpirationPlugin } from 'workbox-expiration';import { initOfflibrary, offlibraryMatch, offlibraryHandler } from 'offlibrary/workbox';
self.skipWaiting();clientsClaim();
// 1. Background Fetch: store finished episode downloads, update the notification.initOfflibrary({ onComplete: (item) => self.registration.showNotification(`${item.title} is ready offline`, { tag: item.id }),});
// 2. The app shell.precacheAndRoute(self.__WB_MANIFEST);registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html')));
// 3. Episodes the user downloaded: from the device, with seeking. Everything else: network.// Registered before the artwork route so stored files are never answered by it.const artwork = new CacheFirst({ cacheName: 'artwork', plugins: [new ExpirationPlugin({ maxEntries: 300, maxAgeSeconds: 30 * 24 * 3600 })],});registerRoute(offlibraryMatch, offlibraryHandler({ fallback: new NetworkOnly() }));
// 4. Cover art and the feed.registerRoute(({ request }) => request.destination === 'image', artwork);registerRoute(({ url }) => url.pathname.endsWith('/feed.json'), new StaleWhileRevalidate({ cacheName: 'feed' }));Why NetworkOnly as the fallback: episodes the user didn’t download should stream from the CDN, not fill a runtime cache with 50 MB files. Artwork that happens to be requested during a cold start is matched by offlibraryMatch for a moment (images are a cold-start destination); if you want it to land in the artwork cache even then, use offlibraryHandler({ fallback: artwork }) for a route limited to images, or see Troubleshooting.
import { createOfflibrary } from 'offlibrary';import { registerSW } from 'virtual:pwa-register'; // vite-plugin-pwa; or navigator.serviceWorker.register('/sw.js')
registerSW({ immediate: true });
const library = createOfflibrary({ namespace: 'podcast', collections: { episodes: { maxItems: 10, ui: { icons: [{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' }] }, clickUrl: (ep) => `/#${ep.id}`, }, },});const episodes = library.collection('episodes');
export const download = (ep) => episodes.download({ id: ep.guid, url: ep.enclosure.url, title: ep.title, size: ep.enclosure.length });
export function play(ep) { audio.src = ep.enclosure.url; // stored → device (206 for seeks); not stored → network audio.play(); episodes.touch(ep.guid);}
episodes.subscribe(() => render(episodes.snapshot()));Check it
Section titled “Check it”- Download an episode; wait for
complete. - DevTools → Network → Offline. Reload: the shell comes from the precache.
- Play the episode and seek: requests show
206from the service worker. - Play an episode you didn’t download: it fails offline, as it should, and streams online.