grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add PWA support with service worker and manifest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+86
+4
app/app.html
··· 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 6 <link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.png" /> 7 + <link rel="manifest" href="%sveltekit.assets%/manifest.json" /> 8 + <meta name="theme-color" content="#000000" /> 9 + <meta name="apple-mobile-web-app-capable" content="yes" /> 10 + <link rel="apple-touch-icon" href="%sveltekit.assets%/icon-192.png" /> 7 11 <title>grain</title> 8 12 <link rel="preconnect" href="https://fonts.googleapis.com" /> 9 13 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+60
app/service-worker.js
··· 1 + import { build, files, version } from "$service-worker"; 2 + 3 + const CACHE = `cache-${version}`; 4 + const ASSETS = new Set([...build, ...files]); 5 + 6 + self.addEventListener("install", (event) => { 7 + event.waitUntil( 8 + caches 9 + .open(CACHE) 10 + .then((cache) => cache.addAll([...ASSETS])) 11 + .then(() => self.skipWaiting()), 12 + ); 13 + }); 14 + 15 + self.addEventListener("activate", (event) => { 16 + event.waitUntil( 17 + caches.keys().then(async (keys) => { 18 + for (const key of keys) { 19 + if (key !== CACHE) await caches.delete(key); 20 + } 21 + self.clients.claim(); 22 + }), 23 + ); 24 + }); 25 + 26 + self.addEventListener("fetch", (event) => { 27 + if (event.request.method !== "GET") return; 28 + 29 + const url = new URL(event.request.url); 30 + 31 + // Skip cross-origin requests 32 + if (url.origin !== self.location.origin) return; 33 + 34 + // For navigation requests, try network first (so new deploys are picked up) 35 + if (event.request.mode === "navigate") { 36 + event.respondWith( 37 + fetch(event.request).catch(() => caches.match(event.request)), 38 + ); 39 + return; 40 + } 41 + 42 + // For assets, use cache-first with network fallback 43 + if (ASSETS.has(url.pathname)) { 44 + event.respondWith( 45 + caches.match(event.request).then((cached) => cached || fetch(event.request)), 46 + ); 47 + return; 48 + } 49 + 50 + // For everything else, network-first with cache fallback 51 + event.respondWith( 52 + fetch(event.request) 53 + .then((response) => { 54 + const clone = response.clone(); 55 + caches.open(CACHE).then((cache) => cache.put(event.request, clone)); 56 + return response; 57 + }) 58 + .catch(() => caches.match(event.request)), 59 + ); 60 + });
static/icon-192.png

This is a binary file and will not be displayed.

static/icon-512.png

This is a binary file and will not be displayed.

+22
static/manifest.json
··· 1 + { 2 + "name": "grain", 3 + "short_name": "grain", 4 + "id": "/", 5 + "start_url": "/", 6 + "scope": "/", 7 + "display": "standalone", 8 + "background_color": "#ffffff", 9 + "theme_color": "#000000", 10 + "icons": [ 11 + { 12 + "src": "/icon-192.png", 13 + "sizes": "192x192", 14 + "type": "image/jpeg" 15 + }, 16 + { 17 + "src": "/icon-512.png", 18 + "sizes": "512x512", 19 + "type": "image/jpeg" 20 + } 21 + ] 22 + }