Devlog #39 – Architektura PWA offline-first i podświetlanie wyników wyszukiwania

Fala 19 kończy się kosmologią, ogólną teorią względności i chemią fizyczną po stronie bloga. Po stronie inżynierskiej w tym cyklu: pełne PWA offline-first z wszystkimi 345 symulacjami pre-cache'owanymi w service workerze v19; niestandardowe podświetlanie wyników wyszukiwania z nawigacją klawiaturową; kompleksowy audyt danych strukturalnych JSON-LD w całej platformie; oraz zaktualizowane kamienie milowe platformy wraz z 39. artykułem „na cenzurowanym” i 19. falą devlogów ukończoną.

Statystyki platformy po Fali 19

345
Symulacji
75
Kategorii
117
Wpisów na blogu
19
Fal treści
39
Artykułów „na cenzurowanym”
29
Wpisów edukacyjnych
39
Devlogów

Retrospektywa Fali 19

Fala 19 zbadała fizykę wszechświata w jego największych skalach i najkrótszych czasach. Artykuł #38 śledził kosmologię inflacyjną przez akustyczne piki CMB aż po symulacje N-ciałowe ciemnej materii i pojawiające się ograniczenia BAO z DESI. Wpis edukacyjny #29 zbudował ogólną teorię względności od równania geodezyjnego aż po czarne dziury Kerra, fale grawitacyjne i paradoks informacji. Artykuł #39 zanurzył się głęboko w chemię fizyczną — teorię stanu przejściowego Eyringa, transfer elektronu Marcusa, DFT i femtosekundową spektroskopię pakietów falowych.

Pełne PWA offline-first

Service worker z Fali 18 pre-cache'ował wszystkie 75 stron indeksowych kategorii, ale nie 345 pojedynczych stron symulacji. Fala 19 rozszerza listę precache o każdy główny plik HTML symulacji, dając odwiedzającym pełną bibliotekę symulacji offline po pierwszej wizycie. Całkowity rozmiar precache to około 12 MB HTML, CSS i JS (bez tekstur WebGL i Three.js, które są zbyt duże, by cache'ować je bezwarunkowo).

Service Worker v19 — strategia precache

// kluczowe zmiany w sw.js w v19

// 1. Manifest budowany podczas builda z listingu katalogu (skrypt Node)
const SIM_PRECACHE = [
  '/acid-base/', '/adc-dac/', '/aerofoil/', ... (345 wpisów)
].map(path => ({ url: path, revision: BUILD_HASH }));

// 2. Instalacja cache z uwzględnieniem limitu pamięci
async function installWithQuotaCheck(cacheName, urls) {
  const estimate = await navigator.storage.estimate();
  const availableMB = (estimate.quota - estimate.usage) / 1e6;
  if (availableMB < 50) {
    console.warn('[SW] Mało miejsca, pomijam precache symulacji');
    return;
  }
  const cache = await caches.open(cacheName);
  // Grupami po 20, aby nie przeciążyć pipeline'u fetch
  for (let i = 0; i < urls.length; i += 20) {
    await cache.addAll(urls.slice(i, i + 20));
  }
}

// 3. CacheFirst dla stron symulacji (długi czas życia cache)
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 dla wpisów bloga (zawsze świeże online)
registerRoute(
  ({ url }) => url.pathname.startsWith('/blog/'),
  new NetworkFirst({ cacheName: 'blog-v19',
    networkTimeoutSeconds: 3,
    plugins: [ new ExpirationPlugin({ maxAgeSeconds: 86400 }) ]
  })
);

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

Wewnątrz strony pojawia się teraz prompt instalacji PWA po 3. odwiedzonej symulacji użytkownika, wykorzystujący zdarzenie beforeinstallprompt. Interfejs pokazuje odrzucalny baner z szacowaną liczbą sesji i argumentacją na rzecz trybu offline. Wczesne dane (symulowane): 6% wskaźnik instalacji na urządzeniach mobilnych, 2% na desktopie.

Podświetlanie wyników wyszukiwania

Rozmyte wyszukiwanie (fuzzy search) wprowadzone w Fali 18 zwraca uszeregowane wyniki symulacji, ale wyświetlało je bez żadnej wizualnej wskazówki, gdzie dopasowało się zapytanie. Fala 19 dodaje podświetlanie fraz: dopasowane słowa i częściowe n-gramy są opakowywane w elementy <mark> z bursztynowym tłem, a skróty klawiszowe F3/Shift+F3 przełączają kolejne podświetlenia na liście wyników.

Niestandardowy podświetlacz DOM-range (bez biblioteki)

// highlight.js — ~60 linii, bez zewnętrznej zależności

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

export function highlightTerms(container, terms) {
  // Przechodzimy przez wszystkie węzły tekstowe pod 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: var(--color-text);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);
  });
}

// Nawigacja klawiaturowa po podświetleniach
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' : '');
});
          

Podświetlacz jest stosowany do tytułu symulacji, kategorii i pól opisu w każdej karcie wyników, wykorzystując tokenizowane frazy zapytania z silnika wyszukiwania rozmytego. Dopasowania trigramów poniżej podobieństwa kosinusowego 0,6 są wyłączone z podświetlania, by uniknąć podświetlania niemal losowych zbieżności znaków.

Audyt danych strukturalnych JSON-LD

Google Rich Results Test wykrył 47 symulacji brakujących wymaganych właściwości schema. Fala 19 dodaje trzy brakujące pola do bloku JSON-LD każdej symulacji: educationalLevel (secondary/undergraduate/graduate), teaches (tablica celów edukacyjnych z opisu symulacji) i learningResourceType (zawsze "simulation" zgodnie ze słownikiem schema.org).

Szablon wzbogacenia JSON-LD (na symulację)

-  "@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
          

Skrypt audytowy Node.js czyta wszystkie 345 plików index.html, wyodrębnia istniejące JSON-LD, waliduje je względem słownika schema.org i generuje manifest łatek. Walidacja po wdrożeniu za pomocą wsadowego inspektora URL w Google Search Console pokazuje 0 błędów i 302 kwalifikujące się do rich results (43 symulacje nie mają istotnych kandydatów na dane edukacyjne i używają wyłącznie WebApplication).

Lista kontrolna inżynierska Fali 19

Zapowiedź Fali 20

⭐ Na cenzurowanym #40

Immunologia i choroby zakaźne

Odporność wrodzona vs nabyta, różnorodność receptorów limfocytów T, mechanizmy szczepionek, odporność stadna R₀ i modelowanie epidemii SIR/SEIR.

📖 Nauka #30

Statystyczna teoria pola

Całki po trajektoriach, funkcje podziału, grupa renormalizacji modelu Isinga, klasy uniwersalności i związki między QFT a mechaniką statystyczną.

⭐ Na cenzurowanym #41

Geofizyka i sejsmologia

Tektonika płyt, propagacja fal sejsmicznych P/S, drgania modów normalnych Ziemi, anomalie geoidy i grawitacji oraz konwekcja płaszcza.

🛠️ Devlog #40

Miniatury wideo i obrazy OG

Zautomatyzowany pipeline zrzutów ekranu oparty o Playwright dla 50+ obrazów OG symulacji, podglądów udostępniania w mediach społecznościowych i generowania miniatur wideo dla kanału bloga.

Fala 20 wyznacza ważny kamień milowy: Nauka #30 będzie 30. wpisem w najdłużej działającej serii edukacyjnej na platformie. Statystyczna teoria pola leży na przecięciu fizyki materii skondensowanej, grawitacji kwantowej i uczenia maszynowego (maszyny Boltzmanna, modele dyfuzyjne) — łącząc pół tuzina innych tematów „na cenzurowanym” w jednych ramach.