Девлог #39 — Офлайн-архітектура PWA та підсвітка результатів пошуку

Хвиля 19 завершується космологією, загальною теорією відносності та фізичною хімією в блозі. З інженерного боку цього циклу: повністю офлайн-орієнтований PWA з усіма 345 симуляціями, попередньо закешованими у service worker v19; власна підсвітка результатів пошуку з навігацією клавіатурою; всебічний аудит структурованих даних JSON-LD по всій платформі; й оновлені етапи розвитку платформи із завершеними 39-м спотлайтом і 19-ю хвилею девлогів.

Статистика платформи після хвилі 19

345
Симуляцій
75
Категорій
117
Статей блогу
19
Хвиль контенту
39
Спотлайтів
29
Навчальних дописів
39
Девлогів

Ретроспектива хвилі 19

Хвиля 19 досліджувала фізику всесвіту в найбільших масштабах і найкоротших часових проміжках. Спотлайт #38 простежив інфляційну космологію через акустичні піки реліктового випромінювання до N-тільних симуляцій темної матерії й нових обмежень DESI BAO. Навчальний #29 побудував загальну теорію відносності від рівняння геодезичної лінії до чорних дір Керра, гравітаційних хвиль та парадоксу інформації. Спотлайт #39 глибоко занурився у фізичну хімію — теорію перехідного стану Айрінга, електронний перенос Маркуса, теорію функціонала густини (DFT) та фемтосекундну спектроскопію хвильових пакетів.

Повністю офлайн-орієнтований PWA

Service worker хвилі 18 попередньо кешував усі 75 сторінок категорій, але не 345 окремих сторінок симуляцій. Хвиля 19 розширює список попереднього кешування на кореневий HTML кожної симуляції, надаючи відвідувачам повну бібліотеку симуляцій офлайн після першого візиту. Загальний обсяг попереднього кешу — близько 12 МБ HTML, CSS і JS (без урахування текстур WebGL і Three.js, які надто великі, щоб кешувати їх безумовно).

Service Worker v19 — стратегія попереднього кешування

// 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();
});
          

Внутрішньосторінкова пропозиція встановлення тепер з'являється після третього відвідування симуляції користувачем через подію beforeinstallprompt. Інтерфейс показує банер, який можна закрити, з приблизною кількістю сеансів та описом переваг офлайн-режиму. Попередні дані (змодельовані): 6% встановлень на мобільних, 2% на десктопі.

Підсвітка результатів пошуку

Нечіткий пошук, представлений у хвилі 18, повертав ранжовані результати симуляцій, але показував їх без жодної візуальної вказівки на те, де саме збігся запит. Хвиля 19 додає підсвітку термінів: збіжні слова й часткові n-грами обгортаються в елементи <mark> із бурштиновим фоном, а комбінації клавіш F3/Shift+F3 циклічно перемикають підсвічені збіги у списку результатів.

Власний DOM-Range підсвічувач (без бібліотек)

// 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: 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);
  });
}

// 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' : '');
});
          

Підсвічувач застосовується до полів назви, категорії й опису симуляції в кожній картці результату, використовуючи токенізовані терміни запиту з нечіткого пошукового рушія. Триграмні збіги з косинусною подібністю нижче 0,6 виключаються з підсвітки, щоб уникнути підсвічування майже випадкових збігів символів.

Аудит структурованих даних JSON-LD

Google Rich Results Test виявив, що 47 симуляціям бракує обов'язкових властивостей схеми. Хвиля 19 додає три відсутні поля до блоку JSON-LD кожної симуляції: educationalLevel (середня школа/бакалаврат/аспірантура), teaches (масив навчальних цілей з опису симуляції) і learningResourceType (завжди "simulation" згідно з термінологією schema.org).

Шаблон збагачення JSON-LD (для кожної симуляції)

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

Скрипт аудиту на Node.js читає всі 345 файлів index.html, витягує наявний JSON-LD, перевіряє його відповідність термінології schema.org і генерує маніфест патчів. Перевірка після розгортання через пакетний інспектор URL-адрес Google Search Console показує 0 помилок і 302 симуляції, придатні для розширених результатів (43 симуляції не мають значних кандидатів у структуровані дані про навчання й використовують лише WebApplication).

Інженерний чекліст хвилі 19

Анонс хвилі 20

⭐ Спотлайт #40

Імунологія та інфекційні хвороби

Вроджений і набутий імунітет, різноманітність рецепторів Т-клітин, механізми вакцин, колективний імунітет (R₀) та епідемічне моделювання SIR/SEIR.

📖 Навчальний #30

Статистична теорія поля

Інтеграли по траєкторіях, статистичні суми, ренормалізаційна група моделі Ізінга, класи універсальності та зв'язки між КТП і статистичною механікою.

⭐ Спотлайт #41

Геофізика та сейсмологія

Тектоніка плит, поширення сейсмічних P/S-хвиль, нормальні моди коливань Землі, аномалії геоїда та гравітації, а також мантійна конвекція.

🛠️ Девлог #40

Відеомініатюри та OG-зображення

Автоматизований конвеєр знімків на базі Playwright для 50+ OG-зображень симуляцій, прев'ю для соцмереж та генерація відеомініатюр для стрічки блогу.

Хвиля 20 знаменує важливу віху: Навчальний #30 стане 30-м дописом найдовшої освітньої серії на платформі. Статистична теорія поля перебуває на перетині фізики конденсованого стану, квантової гравітації та машинного навчання (машини Больцмана, дифузійні моделі) — об'єднуючи півдюжини інших спотлайт-тем в одному фреймворку.