Devlog #39 – PWA Offline-First Architecture & Search Result Highlighting

Wave 19 closes with cosmology, general relativity, and physical chemistry on the blog side. On the engineering side this cycle: a full offline-first PWA with all 345 simulations pre-cached in service worker v19; custom search-result highlighting with keyboard navigation; a comprehensive JSON-LD structured-data audit across the whole platform; and updated platform milestones with the 39th spotlight and 19th devlog wave complete.

Platform Stats after Wave 19

345
Simulations
75
Categories
117
Blog Posts
19
Content Waves
39
Spotlights
29
Learning Posts
39
Devlogs

Wave 19 Retrospective

Wave 19 explored the physics of the universe at its largest scales and smallest times. Spotlight #38 traced inflationary cosmology through the CMB acoustic peaks to dark matter N-body simulations and the emerging DESI BAO constraints. Learning #29 built general relativity from the geodesic equation up to Kerr black holes, gravitational waves, and the information paradox. Spotlight #39 took a deep dive into physical chemistry — Eyring TST, Marcus electron transfer, DFT, and femtosecond wavepacket spectroscopy.

Full Offline-First PWA

The Wave 18 service worker pre-cached all 75 category index pages but not the 345 individual simulation pages. Wave 19 extends the precache list to every simulation root HTML, giving visitors the full simulation library offline after their first visit. The total precache footprint is approximately 12 MB of HTML, CSS, and JS (excluding WebGL textures and Three.js, which are too large to cache unconditionally).

Service Worker v19 — Precache Strategy

// sw.js key changes in v19

// 1. Build-time manifest from directory listing (Node script)
const SIM_PRECACHE = [
  '/acid-base/', '/adc-dac/', '/aerofoil/', ... (345 entries)
].map(path => ({ url: path, revision: BUILD_HASH }));

// 2. Quota-aware cache installation
async function installWithQuotaCheck(cacheName, urls) {
  const estimate = await navigator.storage.estimate();
  const availableMB = (estimate.quota - estimate.usage) / 1e6;
  if (availableMB < 50) {
    console.warn('[SW] Low storage, skipping sim precache');
    return;
  }
  const cache = await caches.open(cacheName);
  // Batch in groups of 20 to avoid overwhelming fetch pipeline
  for (let i = 0; i < urls.length; i += 20) {
    await cache.addAll(urls.slice(i, i + 20));
  }
}

// 3. CacheFirst for simulation pages (long cache lifetime)
registerRoute(
  ({ url }) => SIMS.some(s => url.pathname.startsWith(s)),
  new CacheFirst({ cacheName: 'sims-v19', plugins: [
    new ExpirationPlugin({ maxEntries: 400, maxAgeSeconds: 60 * 60 * 24 * 30 }),
    new CacheableResponsePlugin({ statuses: [200] })
  ]})
);

// 4. NetworkFirst for blog posts (always fresh when online)
registerRoute(
  ({ url }) => url.pathname.startsWith('/blog/'),
  new NetworkFirst({ cacheName: 'blog-v19',
    networkTimeoutSeconds: 3,
    plugins: [ new ExpirationPlugin({ maxAgeSeconds: 86400 }) ]
  })
);

// 5. Offline fallback
setCatchHandler(async ({ request }) => {
  if (request.destination === 'document') return caches.match('/offline.html');
  return Response.error();
});
          

An in-page install prompt is now triggered after a user’s 3rd simulation visit using the beforeinstallprompt event. The UI shows a dismissible banner with estimated session count and offline capability pitch. Early data (simulated): 6% install rate on mobile, 2% on desktop.

Search Result Highlighting

The fuzzy search introduced in Wave 18 returns ranked simulation results but displayed them without any visual indication of where the query matched. Wave 19 adds term highlighting: matched words and partial n-grams are wrapped in <mark> elements with an amber background, and F3/Shift+F3 keyboard shortcuts cycle through highlights in the result list.

Custom DOM-Range Highlighter (no library)

// highlight.js — ~60 lines, no external dependency

function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

export function highlightTerms(container, terms) {
  // Walk all text nodes under container
  const iter = document.createNodeIterator(container, NodeFilter.SHOW_TEXT);
  const nodes = [];
  let n;
  while ((n = iter.nextNode())) nodes.push(n);

  const pattern = new RegExp(
    `(${terms.map(escapeRe).join('|')})`,
    'gi'
  );

  nodes.forEach(textNode => {
    const text = textNode.nodeValue;
    if (!pattern.test(text)) return;
    pattern.lastIndex = 0;

    const frag = document.createDocumentFragment();
    let last = 0, match;
    while ((match = pattern.exec(text)) !== null) {
      if (match.index > last)
        frag.appendChild(document.createTextNode(text.slice(last, match.index)));
      const mark = document.createElement('mark');
      mark.textContent = match[0];
      mark.style.cssText = 'background:#fbbf24;color:#000;border-radius:2px;padding:0 1px';
      frag.appendChild(mark);
      last = pattern.lastIndex;
    }
    if (last < text.length)
      frag.appendChild(document.createTextNode(text.slice(last)));
    textNode.parentNode.replaceChild(frag, textNode);
  });
}

// Keyboard navigation through marks
let markIdx = -1;
document.addEventListener('keydown', e => {
  const isF3 = e.key === 'F3' || (e.ctrlKey && e.key === 'g');
  if (!isF3) return;
  e.preventDefault();
  const marks = [...document.querySelectorAll('#results mark')];
  if (!marks.length) return;
  markIdx = (markIdx + (e.shiftKey ? -1 : 1) + marks.length) % marks.length;
  marks[markIdx].scrollIntoView({ behavior: 'smooth', block: 'center' });
  marks.forEach((m, i) => m.style.outline = i === markIdx ? '2px solid #ff7043' : '');
});
          

The highlighter is applied to simulation title, category, and description fields within each result card, using the tokenised query terms from the fuzzy-search engine. Trigram matches below 0.6 cosine similarity are excluded from highlighting to avoid highlighting near-random character coincidences.

JSON-LD Structured Data Audit

Google’s Rich Results Test found 47 simulations missing required schema properties. Wave 19 adds three missing fields to every simulation’s JSON-LD block: educationalLevel (secondary/undergraduate/graduate), teaches (array of learning objectives from the simulation description), and learningResourceType (always "simulation" per schema.org vocabulary).

JSON-LD Enrichment Template (per simulation)

-  "@type": "WebApplication",
-  "name": "Acid-Base Equilibrium",
-  "description": "Interactive acid-base simulation..."
+  "@type": ["WebApplication", "LearningResource"],
+  "name": "Acid-Base Equilibrium",
+  "description": "Interactive acid-base simulation...",
+  "educationalLevel": "undergraduate",
+  "teaches": [
+    "pH calculation",
+    "Henderson-Hasselbalch equation",
+    "buffer chemistry",
+    "titration curves"
+  ],
+  "learningResourceType": "simulation",
+  "interactivityType": "active",
+  "isAccessibleForFree": true
          

A Node.js audit script reads all 345 index.html files, extracts existing JSON-LD, validates against the schema.org vocabulary, and generates a patch manifest. Post-deployment validation via Google’s Search Console batch URL inspector shows 0 errors and 302 eligible for rich results (43 simulations have no significant educational structured data candidates and use WebApplication only).

Wave 19 Engineering Checklist

Wave 20 Preview

⭐ Spotlight #40

Immunology & Infectious Disease

Innate vs adaptive immunity, T-cell receptor diversity, vaccine mechanisms, herd immunity R₀, and epidemic SIR/SEIR modelling.

📖 Learning #30

Statistical Field Theory

Path integrals, partition functions, Ising model renormalisation group, universality classes, and connections between QFT and statistical mechanics.

⭐ Spotlight #41

Geophysics & Seismology

Plate tectonics, seismic P/S wave propagation, normal mode oscillations of the Earth, geoid and gravity anomalies, and mantle convection.

🛠️ Devlog #40

Video Thumbnails & OG Images

Automated Playwright-based screenshot pipeline for 50+ simulation OG images, social share previews, and video thumbnail generation for the blog feed.

Wave 20 marks an important milestone: Learning #30 will be the 30th post in the longest-running educational series on the platform. Statistical field theory sits at the intersection of condensed matter physics, quantum gravity, and machine learning (Boltzmann machines, diffusion models) — connecting half a dozen other spotlight topics in a single framework.