Konubinix' opinionated web of thoughts

PWA Score Apps — Shared Blocks

Shared noweb blocks for tally_score_tracker.org and score_counter.org. Consumer files use cross-file references like <<pwa_score_apps_shared_blocks.org:head-meta-ex()>>.

Head and PWA configuration

Meta tags

Standard PWA meta tags. The purge script at the top provides a ?purge query parameter escape hatch: it nukes the service worker cache and registrations, then reloads — useful when a bad SW gets stuck in production.


<meta name="theme-color" content="#1a1a2e">
<title>Tally</title>

Import map

All dependencies loaded via ESM from esm.sh. The deps=yjs@13.6.29 pins ensure y-indexeddb, y-websocket and y-protocols all share the exact same Yjs instance — without this, each would bundle its own copy, breaking cross-module instanceof checks.

<script type="importmap">
{"imports":{
  "yjs":"https://esm.sh/yjs@13.6.29",
  "y-indexeddb":"https://esm.sh/y-indexeddb@9?deps=yjs@13.6.29",
  "y-websocket":"https://esm.sh/y-websocket@2?bundle-deps&deps=yjs@13.6.29",
  "lib0/broadcastchannel":"https://esm.sh/lib0@0.2.99/broadcastchannel",
  "lib0/time":"https://esm.sh/lib0@0.2.99/time",
  "lib0/encoding":"https://esm.sh/lib0@0.2.99/encoding",
  "lib0/decoding":"https://esm.sh/lib0@0.2.99/decoding",
  "lib0/observable":"https://esm.sh/lib0@0.2.99/observable",
  "lib0/math":"https://esm.sh/lib0@0.2.99/math",
  "lib0/url":"https://esm.sh/lib0@0.2.99/url",
  "lib0/environment":"https://esm.sh/lib0@0.2.99/environment",
  "y-protocols/sync":"https://esm.sh/y-protocols@1.0.6/sync?deps=yjs@13.6.29",
  "y-protocols/auth":"https://esm.sh/y-protocols@1.0.6/auth?deps=yjs@13.6.29",
  "y-protocols/awareness":"https://esm.sh/y-protocols@1.0.6/awareness?deps=yjs@13.6.29",
  "alpinejs":"https://esm.sh/alpinejs@3"
}}
</script>

Service worker

Network-first strategy for navigation (always fetch the latest HTML), cache-first for static assets (CDN libraries that are versioned in the import map). On install, pre-caches the shell and Alpine. On activate, purges stale caches.

const CACHE = 'tally-v6';
const ASSETS = [
  './',
  './index.html',
  'https://esm.sh/alpinejs@3',
  'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js'
];

self.addEventListener('install', e => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
  self.skipWaiting();
});

self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
    )
  );
  self.clients.claim();
});

self.addEventListener('fetch', e => {
  if (e.request.mode === 'navigate') {
    // Network-first for pages — always get the latest HTML
    e.respondWith(
      fetch(e.request)
        .then(r => caches.open(CACHE).then(c => { c.put(e.request, r.clone()); return r; }))
        .catch(() => caches.match(e.request))
    );
  } else {
    // Cache-first for static assets (CDN libs)
    e.respondWith(
      caches.match(e.request).then(r => r || fetch(e.request))
    );
  }
});

Manifest

{
  "name": "Tally",
  "short_name": "Tally",
  "description": "Score tracker for board games",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#1a1a2e",
  "theme_color": "#1a1a2e",
  "icons": [
    {
      "src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><rect width='512' height='512' rx='96' fill='%231a1a2e'/><text x='256' y='340' font-size='280' text-anchor='middle' fill='%23e94560'>T</text></svg>",
      "sizes": "512x512",
      "type": "image/svg+xml",
      "purpose": "any maskable"
    }
  ]
}

Notes linking here