# HG changeset patch # User MrJuneJune # Date 1771199006 28800 # Node ID 3b47e82ac57e82aade0d1416b814bf1e12080e96 # Parent 5d3e116dd745ab292aa763e7c682f9f21d71fd56 [MrJuneJune] PWA updates. diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/PWA_SETUP.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/PWA_SETUP.md Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,126 @@ +# PWA Setup Guide + +Your site is now configured as a Progressive Web App! 🎉 + +## What's Been Added + +1. **manifest.json** - App configuration +2. **sw.js** - Service worker for caching and offline support +3. **pwa-register.js** - Service worker registration +4. **offline.html** - Offline fallback page +5. **Updated base_head.html** - Links to manifest and PWA script + +## Missing: App Icons + +You need to create PNG icons from your SVG. Run these commands: + +### Using ImageMagick (recommended): + +```bash +cd src/public + +# 192x192 icon +convert epi_all_colors.svg -resize 192x192 -background none icon-192.png + +# 512x512 icon +convert epi_all_colors.svg -resize 512x512 -background none icon-512.png + +# Maskable icon (512x512 with safe zone) +convert epi_all_colors.svg -resize 409x409 -background none -gravity center -extent 512x512 icon-maskable-512.png +``` + +### Or using Inkscape: + +```bash +inkscape epi_all_colors.svg --export-type=png --export-filename=icon-192.png -w 192 -h 192 +inkscape epi_all_colors.svg --export-type=png --export-filename=icon-512.png -w 512 -h 512 +inkscape epi_all_colors.svg --export-type=png --export-filename=icon-maskable-512.png -w 512 -h 512 +``` + +### Or use an online tool: +- https://realfavicongenerator.net/ +- https://www.pwabuilder.com/ + +## Optional: Screenshots (for better app install experience) + +Create screenshots of your site: + +```bash +# Mobile screenshot (540x720) +screenshot-mobile.png + +# Desktop screenshot (1280x720) +screenshot-desktop.png +``` + +You can use browser DevTools to capture these, or remove the `screenshots` section from manifest.json if you don't want them. + +## Testing Your PWA + +1. **Serve over HTTPS** - PWAs require HTTPS (localhost works for testing) +2. **Open Chrome DevTools** → Application tab → Manifest +3. **Check Service Worker** → Application tab → Service Workers +4. **Lighthouse Audit** → Run PWA audit to see score + +## Install Prompt + +When users visit your site, they'll see an "Install App" button in the bottom-right corner for 10 seconds. They can: +- Click it to install immediately +- Use browser menu: "Install MrJuneJune" or "Add to Home Screen" + +## Features + +✅ **Offline Support** - Caches pages, CSS, JS, fonts, images +✅ **App Shortcuts** - Quick access to Blog and Notes +✅ **Install Prompt** - Automatic install button +✅ **Auto-updates** - Service worker updates on reload +✅ **Fast Loading** - Cached resources load instantly + +## Customization + +Edit `manifest.json` to change: +- `theme_color` - App theme color +- `background_color` - Splash screen color +- `display` - `standalone`, `fullscreen`, `minimal-ui`, or `browser` +- `shortcuts` - App shortcut menu items + +Edit `sw.js` to change: +- `CACHE_VERSION` - Increment to force cache refresh +- `STATIC_CACHE` - Files to cache immediately +- Caching strategy (currently: cache-first with network fallback) + +## Testing on Mobile + +### Android: +1. Open Chrome +2. Visit your site +3. Tap menu → "Install app" or "Add to Home Screen" + +### iOS (Safari): +1. Open Safari +2. Tap Share button +3. Tap "Add to Home Screen" + +Note: iOS has limited PWA support (no install prompt, limited background features) + +## Debugging + +Check console for: +- `[PWA]` - Registration events +- `[SW]` - Service worker caching events + +Clear cache: +```js +navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' }); +``` + +## Next Steps + +1. Generate the PNG icons (see commands above) +2. Test on HTTPS +3. Run Lighthouse audit +4. Deploy and test on mobile device +5. Consider adding: + - Push notifications + - Background sync + - Share target API diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/generate-icons.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/generate-icons.sh Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,42 @@ +#!/bin/bash + +# Generate PWA icons from SVG +# Requires ImageMagick: apt-get install imagemagick + +cd "$(dirname "$0")/src/public" + +if ! command -v convert &> /dev/null; then + echo "❌ ImageMagick not found. Please install it:" + echo " Ubuntu/Debian: sudo apt-get install imagemagick" + echo " macOS: brew install imagemagick" + echo "" + echo "Or use an online tool: https://realfavicongenerator.net/" + exit 1 +fi + +echo "🎨 Generating PWA icons from epi_all_colors.svg..." + +# 192x192 icon +echo " → Creating icon-192.png..." +convert epi_all_colors.svg -resize 192x192 -background none icon-192.png + +# 512x512 icon +echo " → Creating icon-512.png..." +convert epi_all_colors.svg -resize 512x512 -background none icon-512.png + +# Maskable icon (512x512 with safe zone - 80% of size) +echo " → Creating icon-maskable-512.png..." +convert epi_all_colors.svg -resize 409x409 -background none -gravity center -extent 512x512 icon-maskable-512.png + +echo "" +echo "✅ Icons generated successfully!" +echo "" +echo "Generated files:" +echo " - icon-192.png" +echo " - icon-512.png" +echo " - icon-maskable-512.png" +echo "" +echo "Next steps:" +echo " 1. Verify icons look good: ls -lh src/public/icon-*.png" +echo " 2. Test PWA: Open site in Chrome → DevTools → Application → Manifest" +echo " 3. Run Lighthouse audit to check PWA score" diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/base.css --- a/mrjunejune/src/base.css Sun Feb 15 12:33:54 2026 -0800 +++ b/mrjunejune/src/base.css Sun Feb 15 15:43:26 2026 -0800 @@ -359,3 +359,40 @@ } +/* pwa icon */ +#pwa-install-btn { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 24px 12px 36px; + background: #000000; + color: #eeffee; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + z-index: 1000; + font-family: inherit; +} + +#pwa-install-btn.with-icon::before { + content: ""; + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + + /* Masked icon */ + -webkit-mask: url('/public/paw.svg') no-repeat center; + mask: url('/public/paw.svg') no-repeat center; + mask-size: contain; + mask-repeat: no-repeat; + + /* Icon color = button text color */ + background-color: currentColor; +} + diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/offline.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/offline.html Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,61 @@ + + + + + + Offline - MrJuneJune + + + +
📡
+

You're Offline

+

It looks like you've lost your internet connection.
Don't worry, some cached pages might still work!

+ + + diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/parts/base_head.html --- a/mrjunejune/src/parts/base_head.html Sun Feb 15 12:33:54 2026 -0800 +++ b/mrjunejune/src/parts/base_head.html Sun Feb 15 15:43:26 2026 -0800 @@ -1,6 +1,9 @@ + + + @@ -16,3 +19,5 @@ + + diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/icon-192.png Binary file mrjunejune/src/public/icon-192.png has changed diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/icon-512.png Binary file mrjunejune/src/public/icon-512.png has changed diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/icon-maskable-512.png Binary file mrjunejune/src/public/icon-maskable-512.png has changed diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/manifest.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/manifest.json Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,67 @@ +{ + "name": "MrJuneJune", + "short_name": "MrJuneJune", + "description": "Personal website and blog of Juntae (June)", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#2337ff", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/public/epi_all_colors.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + }, + { + "src": "/public/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/public/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/public/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "screenshots": [ + { + "src": "/public/screenshot-mobile.png", + "sizes": "540x720", + "type": "image/png", + "form_factor": "narrow" + }, + { + "src": "/public/screenshot-desktop.png", + "sizes": "1280x720", + "type": "image/png", + "form_factor": "wide" + } + ], + "categories": ["lifestyle", "productivity"], + "shortcuts": [ + { + "name": "Blog", + "short_name": "Blog", + "description": "Read latest blog posts", + "url": "/blog", + "icons": [{ "src": "/public/epi_all_colors.svg", "sizes": "192x192" }] + }, + { + "name": "Notes", + "short_name": "Notes", + "description": "Access your notes", + "url": "/notes", + "icons": [{ "src": "/public/epi_all_colors.svg", "sizes": "192x192" }] + } + ] +} diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/pwa-register.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/pwa-register.js Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,88 @@ +// PWA Service Worker Registration +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker + .register('/public/sw.js') + .then((registration) => { + console.log('[PWA] Service Worker registered:', registration.scope); + + // Check for updates periodically + setInterval(() => { + registration.update(); + }, 60000); // Check every minute + + // Handle updates + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing; + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + // New content available, show update notification + if (confirm('New version available! Reload to update?')) { + newWorker.postMessage({ type: 'SKIP_WAITING' }); + window.location.reload(); + } + } + }); + }); + }) + .catch((error) => { + console.error('[PWA] Service Worker registration failed:', error); + }); + + // Handle service worker updates + let refreshing = false; + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (!refreshing) { + refreshing = true; + window.location.reload(); + } + }); + }); +} + +// Add install prompt handler +let deferredPrompt; + +window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredPrompt = e; + + showInstallPromotion(); +}); + +function showInstallPromotion() { + // Create an install button if it doesn't exist + if (document.getElementById('pwa-install-btn')) return; + + const installBtn = document.createElement('button'); + installBtn.classList.add('with-icon'); + installBtn.id = 'pwa-install-btn'; + installBtn.textContent = 'Install as App'; + + installBtn.addEventListener('click', async () => { + if (!deferredPrompt) return; + + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + console.log(`[PWA] User response: ${outcome}`); + + deferredPrompt = null; + installBtn.remove(); + }); + + document.body.appendChild(installBtn); + + // setTimeout(() => { + // installBtn.style.opacity = '0'; + // installBtn.style.transition = 'opacity 0.3s'; + // setTimeout(() => installBtn.remove(), 300); + // }, 10000); +} + +window.addEventListener('appinstalled', () => { + console.log('[PWA] App installed successfully!'); + deferredPrompt = null; + + const installBtn = document.getElementById('pwa-install-btn'); + if (installBtn) installBtn.remove(); +}); diff -r 5d3e116dd745 -r 3b47e82ac57e mrjunejune/src/public/sw.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/sw.js Sun Feb 15 15:43:26 2026 -0800 @@ -0,0 +1,138 @@ +// Service Worker for MrJuneJune PWA +const CACHE_VERSION = 'v1'; +const CACHE_NAME = `mrjunejune-${CACHE_VERSION}`; + +// Files to cache immediately on install +const STATIC_CACHE = [ + '/', + '/base.css', + '/public/epi_all_colors.svg', + '/public/fonts/Roboto-Regular.ttf', + '/public/fonts/Roboto-Thin.ttf', + '/public/fonts/more-sugar.regular.otf', + '/public/fonts/more-sugar.thin.otf', +]; + +// Install event - cache static assets +self.addEventListener('install', (event) => { + console.log('[SW] Installing service worker...'); + + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + console.log('[SW] Caching static assets'); + return cache.addAll(STATIC_CACHE); + }).then(() => { + console.log('[SW] Skip waiting'); + return self.skipWaiting(); + }) + ); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + console.log('[SW] Activating service worker...'); + + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + console.log('[SW] Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }).then(() => { + console.log('[SW] Claiming clients'); + return self.clients.claim(); + }) + ); +}); + +// Fetch event - serve from cache, fallback to network +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // Skip non-GET requests + if (request.method !== 'GET') { + return; + } + + // Skip chrome-extension and other non-http(s) requests + if (!url.protocol.startsWith('http')) { + return; + } + + // Skip API calls and media uploads (always go to network) + if (url.pathname.startsWith('/api/')) { + return; + } + + event.respondWith( + caches.match(request).then((cachedResponse) => { + if (cachedResponse) { + console.log('[SW] Serving from cache:', url.pathname); + return cachedResponse; + } + + // Not in cache, fetch from network + return fetch(request).then((networkResponse) => { + // Only cache successful responses + if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') { + return networkResponse; + } + + // Cache specific file types + const shouldCache = + url.pathname.endsWith('.css') || + url.pathname.endsWith('.js') || + url.pathname.endsWith('.svg') || + url.pathname.endsWith('.png') || + url.pathname.endsWith('.jpg') || + url.pathname.endsWith('.webp') || + url.pathname.endsWith('.woff') || + url.pathname.endsWith('.woff2') || + url.pathname.endsWith('.ttf') || + url.pathname.endsWith('.otf') || + url.pathname.startsWith('/blog/') || + url.pathname.startsWith('/notes/') || + url.pathname === '/'; + + if (shouldCache) { + const responseToCache = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + console.log('[SW] Caching new resource:', url.pathname); + cache.put(request, responseToCache); + }); + } + + return networkResponse; + }).catch((error) => { + console.log('[SW] Fetch failed:', error); + + // Return offline page for HTML requests + if (request.headers.get('Accept').includes('text/html')) { + return caches.match('/offline.html'); + } + }); + }) + ); +}); + +// Handle messages from the client +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'CLEAR_CACHE') { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => caches.delete(cacheName)) + ); + }) + ); + } +});