Blame · Line-by-line history
pwa.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| eae38d1 | 1 | /** |
| 2 | * Block G1 — PWA (progressive web app) support. | |
| 3 | * | |
| 4 | * GET /manifest.webmanifest — app manifest (install prompt) | |
| 5 | * GET /sw.js — service worker (cache-first for static, network-first for HTML) | |
| 6 | * GET /icon.svg — monochrome logo used by the manifest | |
| 7 | * | |
| 8 | * The service worker deliberately keeps the cache small (static CSS-in-JS is | |
| 9 | * inlined so there's nothing to cache beyond the manifest + icon). HTML pages | |
| 10 | * fall through to the network; cached copies only serve offline fallback. | |
| 11 | * | |
| 12 | * Adding `<link rel="manifest" href="/manifest.webmanifest">` + a tiny SW | |
| 13 | * registration snippet to `Layout` turns any repo page into an installable | |
| 14 | * PWA on Chrome/Safari. | |
| 15 | */ | |
| 16 | ||
| 17 | import { Hono } from "hono"; | |
| 18 | ||
| 19 | const pwa = new Hono(); | |
| 20 | ||
| 21 | export const MANIFEST = { | |
| 22 | name: "Gluecron", | |
| 23 | short_name: "Gluecron", | |
| 24 | description: "AI-native code intelligence + git hosting", | |
| 25 | start_url: "/", | |
| 26 | scope: "/", | |
| 27 | display: "standalone", | |
| 28 | background_color: "#0d1117", | |
| 29 | theme_color: "#0d1117", | |
| 30 | icons: [ | |
| 31 | { | |
| 32 | src: "/icon.svg", | |
| 33 | sizes: "any", | |
| 34 | type: "image/svg+xml", | |
| 35 | purpose: "any maskable", | |
| 36 | }, | |
| 37 | ], | |
| 38 | categories: ["developer", "productivity"], | |
| 39 | } as const; | |
| 40 | ||
| 41 | pwa.get("/manifest.webmanifest", (c) => { | |
| 42 | c.header("content-type", "application/manifest+json"); | |
| 43 | c.header("cache-control", "public, max-age=3600"); | |
| 44 | return c.body(JSON.stringify(MANIFEST)); | |
| 45 | }); | |
| 46 | ||
| 47 | const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> | |
| 48 | <rect width="128" height="128" rx="24" fill="#0d1117"/> | |
| 49 | <g fill="#58a6ff" font-family="monospace" font-size="58" font-weight="700" text-anchor="middle"> | |
| 50 | <text x="64" y="82">gc</text> | |
| 51 | </g> | |
| 52 | <circle cx="28" cy="28" r="5" fill="#3fb950"/> | |
| 53 | </svg>`; | |
| 54 | ||
| 55 | pwa.get("/icon.svg", (c) => { | |
| 56 | c.header("content-type", "image/svg+xml"); | |
| 57 | c.header("cache-control", "public, max-age=86400, immutable"); | |
| 58 | return c.body(ICON_SVG); | |
| 59 | }); | |
| 60 | ||
| 61 | /** | |
| e1c7aa6 | 62 | * Bare-bones service worker — v4 NUKE EDITION. |
| 63 | * | |
| 64 | * After repeated reports of stale HTML being served from old cache versions, | |
| 65 | * this SW does ONE job: unregister itself and purge every cache. Browsers | |
| 66 | * that previously installed v1/v2/v3 will load this v4, see the unregister | |
| 67 | * call, and stop intercepting fetches. From now on EVERY page load goes | |
| 68 | * straight to the network — no SW, no cache, instant fresh content on push. | |
| 69 | * | |
| 70 | * Once we trust the auto-deploy pipeline + want offline support back, ship | |
| 71 | * a real SW with conservative network-first behaviour. Until then: instant | |
| 72 | * deploys win. | |
| eae38d1 | 73 | */ |
| e1c7aa6 | 74 | export const SERVICE_WORKER_SRC = `// gluecron service worker — v4 (self-nuke) |
| 75 | self.addEventListener('install', () => self.skipWaiting()); | |
| eae38d1 | 76 | self.addEventListener('activate', (e) => { |
| e1c7aa6 | 77 | e.waitUntil((async () => { |
| 78 | // Purge every cache from any prior SW version | |
| 79 | const keys = await caches.keys(); | |
| 80 | await Promise.all(keys.map((k) => caches.delete(k))); | |
| 81 | // Tell every client we're done so they reload onto clean network state | |
| 82 | const clients = await self.clients.matchAll({ type: 'window' }); | |
| 83 | for (const c of clients) c.navigate(c.url).catch(() => {}); | |
| 84 | // Then unregister this SW so future loads skip the SW layer entirely | |
| 85 | await self.registration.unregister(); | |
| 86 | })()); | |
| eae38d1 | 87 | }); |
| e1c7aa6 | 88 | // No fetch handler — every request goes straight to the network. |
| eae38d1 | 89 | `; |
| 90 | ||
| 91 | pwa.get("/sw.js", (c) => { | |
| 92 | c.header("content-type", "application/javascript"); | |
| e1c7aa6 | 93 | // No-cache: browser must check on every page load. Critical for the v4 |
| 94 | // self-nuke SW to actually reach all returning visitors. | |
| 95 | c.header("cache-control", "no-cache, no-store, must-revalidate"); | |
| 96 | c.header("pragma", "no-cache"); | |
| eae38d1 | 97 | // Service-Worker-Allowed required for root-scope SW served from root |
| 98 | c.header("service-worker-allowed", "/"); | |
| 99 | return c.body(SERVICE_WORKER_SRC); | |
| 100 | }); | |
| 101 | ||
| 102 | /** | |
| 103 | * Inline script registering the SW. Loaded once at the bottom of every page. | |
| 104 | * Kept tiny so we don't bloat TTI. | |
| 105 | */ | |
| 106 | export const PWA_REGISTER_SNIPPET = ` | |
| 107 | if ('serviceWorker' in navigator) { | |
| 108 | window.addEventListener('load', function() { | |
| 109 | navigator.serviceWorker.register('/sw.js').catch(function() {}); | |
| 110 | }); | |
| 111 | } | |
| 112 | `.trim(); | |
| 113 | ||
| 114 | export default pwa; |