PWA Configuration
Setting up Progressive Web App features for BrewHoard
This guide covers configuring Progressive Web App (PWA) features for BrewHoard, enabling offline functionality and native app-like experiences.
Service Worker Setup
Create a service worker for caching and offline support:
JavaScript
// src/lib/pwa/service-worker.js
import { build, files, version } from '$service-worker';
const CACHE_NAME = `brewhoard-${version}`;
const ASSETS = [
...build,
...files
];
// Install event - cache assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS);
})
);
self.skipWaiting();
});
// Activate event - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch event - serve from cache or network
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then((response) => {
// Cache successful GET requests
if (response.status === 200 && event.request.url.startsWith(self.location.origin)) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
});
})
);
});Register the service worker in your app:
JavaScript
// src/hooks.client.js
import { dev } from '$app/environment';
if (!dev && 'serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then((registration) => {
console.log('Service worker registered:', registration);
})
.catch((error) => {
console.log('Service worker registration failed:', error);
});
}Manifest.json
Create a web app manifest for PWA installation:
JSON
{
"name": "BrewHoard",
"short_name": "BrewHoard",
"description": "Track and manage your beer collection",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1f2937",
"orientation": "portrait-primary",
"categories": ["lifestyle", "productivity"],
"lang": "en-US",
"icons": [
{
"src": "/pwa-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/screenshot-mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/screenshot-desktop.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}Place this file in your static/ directory as manifest.json.
Offline Support
Implement offline functionality with cached data:
JavaScript
// src/lib/pwa/offline-manager.js
import { browser } from '$app/environment';
class OfflineManager {
constructor() {
if (browser) {
this.db = null;
this.initDB();
}
}
async initDB() {
if (!('indexedDB' in window)) return;
const request = indexedDB.open('BrewHoardOffline', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('beers')) {
db.createObjectStore('beers', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('collection')) {
db.createObjectStore('collection', { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
};
}
async saveBeer(beer) {
if (!this.db) return;
const transaction = this.db.transaction(['beers'], 'readwrite');
const store = transaction.objectStore('beers');
await store.put(beer);
}
async getBeers() {
if (!this.db) return [];
const transaction = this.db.transaction(['beers'], 'readonly');
const store = transaction.objectStore('beers');
return new Promise((resolve) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
});
}
}
export const offlineManager = new OfflineManager();Caching Strategies
Implement different caching strategies for different resource types:
JavaScript
// Cache-first strategy for static assets
const cacheFirst = async (request) => {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
return fetch(request);
};
// Network-first strategy for dynamic content
const networkFirst = async (request) => {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
return networkResponse;
}
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
};
// Stale-while-revalidate strategy
const staleWhileRevalidate = async (request) => {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then((networkResponse) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchPromise;
};Install Prompt
Handle PWA installation prompts:
Svelte
<!-- src/components/pwa/InstallPrompt.svelte -->
<script>
import { onMount } from 'svelte';
import { browser } from '$app/environment';
let deferredPrompt;
let showInstallButton = false;
onMount(() => {
if (!browser) return;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallButton = true;
});
window.addEventListener('appinstalled', () => {
showInstallButton = false;
deferredPrompt = null;
});
});
async function installApp() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
}
deferredPrompt = null;
showInstallButton = false;
}
</script>
{#if showInstallButton}
<button on:click={installApp} class="install-button">
Install BrewHoard
</button>
{/if}
<style>
.install-button {
position: fixed;
bottom: 20px;
right: 20px;
background: #1f2937;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>Push Notifications
Set up push notifications for updates and reminders:
JavaScript
// src/lib/pwa/push-manager.js
import { browser } from '$app/environment';
class PushManager {
constructor() {
if (browser) {
this.registration = null;
this.init();
}
}
async init() {
if ('serviceWorker' in navigator) {
this.registration = await navigator.serviceWorker.ready;
}
}
async requestPermission() {
if (!('Notification' in window)) return false;
const permission = await Notification.requestPermission();
return permission === 'granted';
}
async subscribe() {
if (!this.registration) return null;
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
subscription: await this.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
})
})
});
return response.json();
}
urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
export const pushManager = new PushManager();App Icons
Generate and configure app icons for different sizes:
- Create icons in these sizes: 192x192, 512x512
- Include both regular and maskable versions
- Place in
static/directory - Reference in
manifest.json
Example icon generation script:
Bash
# Using ImageMagick
convert source-icon.png -resize 192x192 static/pwa-192x192.png
convert source-icon.png -resize 512x512 static/pwa-512x512.png
# For maskable icons (with padding)
convert source-icon.png -resize 144x144 -background transparent -gravity center -extent 192x192 static/pwa-192x192-maskable.pngNext Steps
- Production Deployment - Deploy to production servers
- Development Setup - Local development environment
- API Documentation - REST API reference