Offline-First App Example
Build an app that works fully offline.
Configuration
// zephyrConfig.js
importScripts('https://unpkg.com/@maravilla-labs/zephyr@0.2.0/lib/zephyrWorker.js');
const config = {
rules: [
// App shell (HTML, CSS, JS)
{
test: '.*\\.(html|css|js)$',
method: 'GET',
cache: 1440,
maxEntries: 50,
fallback: {
strategy: 'stale-if-error',
maxStaleAge: 10080 // Serve stale up to 1 week
}
},
// Images
{
test: '.*\\.(png|jpg|jpeg|gif|webp|svg|ico)$',
method: 'GET',
cache: 1440,
maxEntries: 200,
fallback: {
strategy: 'stale-if-error',
maxStaleAge: 10080
}
},
// API data
{
test: '.*\\/api\\/.*',
method: 'GET',
cache: 60,
maxEntries: 100,
fallback: {
strategy: 'stale-if-error',
maxStaleAge: 1440 // Use day-old data if offline
}
},
// User data (POST)
{
test: '.*\\/api\\/user\\/.*',
method: 'POST',
cache: 30,
maxEntries: 50,
fallback: {
strategy: 'stale-if-error',
maxStaleAge: 1440
}
}
],
// Precache critical assets on install
precache: {
urls: [
'/',
'/index.html',
'/css/app.css',
'/js/app.js',
'/images/logo.png',
'/api/config'
]
},
quota: {
maxSize: 100 * 1024 * 1024,
warningThreshold: 0.8,
onQuotaExceeded: 'evict-lru'
}
};
initZephyr(config);
HTML Setup
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline App</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<div id="app">
<header>
<img src="/images/logo.png" alt="Logo">
<span id="online-status"></span>
</header>
<main id="content"></main>
</div>
<script src="/js/app.js"></script>
<script type="module" src="https://unpkg.com/@maravilla-labs/zephyr@0.2.0/lib/zephrInstall.js"></script>
</body>
</html>
Offline Detection
// app.js
const statusEl = document.getElementById('online-status');
function updateOnlineStatus() {
if (navigator.onLine) {
statusEl.textContent = 'Online';
statusEl.className = 'status-online';
} else {
statusEl.textContent = 'Offline (cached data)';
statusEl.className = 'status-offline';
}
}
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
updateOnlineStatus();
Optimistic Updates
Handle offline writes with optimistic UI:
async function saveData(data) {
// Update UI immediately
updateUI(data);
try {
const response = await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Save failed');
showNotification('Saved successfully');
} catch (error) {
if (!navigator.onLine) {
// Queue for later sync
queueForSync(data);
showNotification('Saved locally, will sync when online');
} else {
showNotification('Save failed', 'error');
revertUI();
}
}
}
// Background sync queue
const syncQueue = [];
function queueForSync(data) {
syncQueue.push(data);
localStorage.setItem('syncQueue', JSON.stringify(syncQueue));
}
window.addEventListener('online', async () => {
const queue = JSON.parse(localStorage.getItem('syncQueue') || '[]');
for (const data of queue) {
try {
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} catch (e) {
console.error('Sync failed for item:', data);
}
}
localStorage.removeItem('syncQueue');
showNotification('Data synced');
});
Precache on Install
Ensure critical assets are available immediately:
zephyr.onPrecacheComplete((event) => {
console.log(`Precached ${event.succeeded}/${event.total} assets`);
if (event.failed > 0) {
console.warn(`Failed to precache ${event.failed} assets`);
}
});
Testing Offline
- Load your app with network enabled
- Open DevTools → Application → Service Workers
- Check "Offline" checkbox
- Navigate around - app should work
- Try saving data - should queue for sync
- Uncheck "Offline"
- Data should sync automatically
Cache Status UI
Show cache status to users:
async function showCacheStatus() {
const stats = await zephyr.stats();
const quota = await zephyr.quota();
document.getElementById('cache-status').innerHTML = `
<p>Cached: ${stats.entries} items (${stats.storageUsedMB})</p>
<p>Storage: ${quota.percentage} used</p>
<p>Hit rate: ${stats.hitRate}</p>
<button onclick="clearCache()">Clear Cache</button>
`;
}
async function clearCache() {
await zephyr.clear();
showNotification('Cache cleared');
showCacheStatus();
}
Progressive Enhancement
Ensure the app works without service worker:
// Check for service worker support
if ('serviceWorker' in navigator) {
// Full offline experience
enableOfflineFeatures();
} else {
// Graceful degradation
showNotification('Offline mode not supported in this browser');
}
