This document describes KanaDojo's Progressive Web App (PWA) configuration and service worker implementation.
KanaDojo is configured as a Progressive Web App (PWA) with:
- Offline support: Audio files and translations cached for offline use
- Fast loading: Service worker caches assets and API responses
- Installable: Can be installed as an app on mobile and desktop
The service worker is located at public/sw.js.
- Audio caching: Caches sound effects for instant playback
- Translation caching: Caches API responses for offline translation
- Text analysis caching: Caches analysis results
- Offline fallback: Provides basic translations when offline
| Cache | Purpose | Version |
|---|---|---|
audio-cache-v2 |
Audio files | v2 |
kanadojo-api-v1 |
API responses | v1 |
kanadojo-static-v1 |
Static assets | v1 |
Request → Cache → Return cached response
→ If miss: Fetch → Cache → Return network response
Rationale: Audio files don't change often, so cache-first provides fastest playback.
Request → Network → If success: Cache → Return response
→ If fail: Check cache → Return cached or error
Rationale: Translations may update, so we prefer fresh data but fall back to cache.
The service worker includes a built-in dictionary of common translations:
const OFFLINE_TRANSLATIONS = {
'en:ja': {
hello: 'こんにちは',
'thank you': 'ありがとう',
goodbye: 'さようなら',
// ... more phrases
},
'ja:en': {
こんにちは: 'hello',
ありがとう: 'thank you',
さようなら: 'goodbye',
// ... more phrases
},
};| Scenario | Behavior |
|---|---|
| Translation cached | Returns cached response |
| Translation not cached, offline fallback exists | Returns offline fallback |
| No cache or fallback | Returns 503 error |
| Audio file cached | Returns cached audio |
| Audio file not cached, offline | Returns 503 error |
public/manifest.json
{
"name": "KanaDojo",
"short_name": "KanaDojo",
"description": "Learn Japanese with gamified training",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#667eea",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Add these to manifest.json:
{
"categories": ["education", "games"],
"orientation": "portrait-primary",
"screenshots": [
{
"src": "/screenshots/home.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"shortcuts": [
{
"name": "Practice Kana",
"short_name": "Kana",
"description": "Start a Kana practice session",
"url": "/kana",
"icons": [{ "src": "/icons/kana.png", "sizes": "192x192" }]
},
{
"name": "Practice Kanji",
"short_name": "Kanji",
"description": "Start a Kanji practice session",
"url": "/kanji",
"icons": [{ "src": "/icons/kanji.png", "sizes": "192x192" }]
}
]
}Update public/sw.js to cache static assets:
// Add after API_CACHE_NAME
const STATIC_CACHE_NAME = 'kanadojo-static-v1';
// Add to fetch handler
if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirst(event.request, STATIC_CACHE_NAME));
}For offline translation queue:
self.addEventListener('sync', function (event) {
if (event.tag === 'sync-translations') {
event.waitUntil(syncPendingTranslations());
}
});// Request permission
Notification.requestPermission();
// Handle push
self.addEventListener('push', function (event) {
const data = event.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
});
});In a client component:
'use client';
import { useEffect, useState } from 'react';
export function PWAInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('Install outcome:', outcome);
setDeferredPrompt(null);
};
if (!deferredPrompt) return null;
return (
<button onClick={handleInstall}>
Install KanaDojo App
</button>
);
}For sharing content to KanaDojo:
{
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}- Open DevTools → Application tab
- Check Service Workers status
- Test offline mode
- Check cache storage
Run Lighthouse PWA audit:
npm run build
npm run start
# Open http://localhost:3000
# Run Lighthouse audit- Check browser console for errors
- Verify
sw.jsis served correctly - Ensure HTTPS or localhost
- Check cache name matches
- Verify response is OK (status 200)
- Check for CORS issues
- Check manifest.json is valid
- Verify icons are accessible
- Ensure user hasn't dismissed prompt
Last Updated: January 2025