diff --git a/README.md b/README.md
index 6b06864..03990f5 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,9 @@ Sağ üstteki **Export** düğmesiyle mevcut sekme içeriği şu biçimlerde dı
### Dil Desteği
Sağ üstteki **EN / TR** geçişiyle arayüz dili değiştirilebilir. Tercih `localStorage`'a kaydedilir.
+### Örnekleri yeniden yükleme
+Üst bardaki **Örnekler** düğmesi güncel dildeki örnek içeriği yeni bir notebook olarak açar. Mevcut notebook'ları veya `localStorage` verisini silmez; böylece daha önce kullanıcı verisi kaydedilmiş olsa bile örnekler tekrar yüklenebilir.
+
### Offline durum, yedekleme ve kayıt
- Alt durum çubuğu tarayıcının çevrimiçi / çevrimdışı durumunu anlık gösterir.
- **Yedek** düğmesi tüm notebook'ları, aktif sekmeyi ve temel görünüm tercihlerini tek bir sürümlü JSON dosyasında dışa aktarır.
@@ -153,6 +156,7 @@ GitHub Pages sürümü installable PWA olarak yayınlanır. Manifest, uygulama i
- `start_url` ve `scope` relative (`./`) tutulduğu için proje `/mathnote/` gibi GitHub Pages alt yollarında çalışır.
- İlk başarılı ziyarette uygulama shell'i, JavaScript/CSS, KaTeX fontları, manifest ve ikonlar precache edilir.
+- `app.js`, `app.css` ve manifest gibi değişebilir çekirdek dosyalar ağ varken **network-first**, çevrimdışıyken cache fallback ile yüklenir. Bu, yeni deploy sonrasında eski JavaScript'in yanlışlıkla servis edilmesini önler.
- Yeni build'de dosya içeriğinden yeni bir cache sürümü üretilir; service worker aktive olduğunda eski MathNote cache'leri temizlenir.
- Service worker yalnızca HTTP/HTTPS üzerinde kaydolur. Bu nedenle `dist/index.html` dosyasını doğrudan `file://` ile açma desteği korunur.
- Destekleyen tarayıcılar MathNote'u masaüstüne veya ana ekrana bağımsız uygulama olarak kurabilir.
diff --git a/index.html b/index.html
index a5aa175..334bae5 100644
--- a/index.html
+++ b/index.html
@@ -66,7 +66,7 @@
border-radius: 6px;
padding: 2px;
}
-#export-btn, #backup-btn, #info-btn {
+#export-btn, #backup-btn, #examples-btn, #info-btn {
padding: 4px 10px;
border: 1px solid #ced4da;
background: #fff;
@@ -82,7 +82,7 @@
gap: 4px;
white-space: nowrap;
}
-#export-btn:hover, #backup-btn:hover, #info-btn:hover {
+#export-btn:hover, #backup-btn:hover, #examples-btn:hover, #info-btn:hover {
background: #f1f3f5;
border-color: #1A71C2;
color: #1A71C2;
@@ -968,6 +968,7 @@
mathnote
+
diff --git a/scripts/verify-offline.mjs b/scripts/verify-offline.mjs
index 97d3e4e..15fcdd4 100644
--- a/scripts/verify-offline.mjs
+++ b/scripts/verify-offline.mjs
@@ -60,11 +60,19 @@ if (!html.includes('rel="manifest" href="./manifest.webmanifest"')) {
violations.push('index.html: manifest link missing or not relative');
}
+if (!html.includes('id="examples-btn"')) {
+ violations.push('index.html: examples button missing');
+}
+
const app = await readFile(new URL('app.js', root), 'utf8');
if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) {
violations.push('app.js: relative service worker registration missing');
}
+if (!app.includes('examples-btn') || !app.includes('getDefaultContent')) {
+ violations.push('app.js: examples loader wiring missing');
+}
+
const manifest = JSON.parse(await readFile(new URL('manifest.webmanifest', root), 'utf8'));
if (manifest.start_url !== './') violations.push('manifest: start_url must be ./');
if (manifest.scope !== './') violations.push('manifest: scope must be ./');
@@ -81,6 +89,13 @@ if (sw.includes('__CACHE_NAME__') || sw.includes('__PRECACHE_ASSETS__')) {
if (!/mathnote-[a-f0-9]{12}/.test(sw)) {
violations.push('sw.js: content-derived cache version missing');
}
+
+if (!sw.includes('NETWORK_FIRST_PATHS') || !sw.includes('networkFirst(request')) {
+ violations.push('sw.js: mutable core assets are not network-first');
+}
+if (sw.includes('caches.match(')) {
+ violations.push('sw.js: cross-cache lookup can serve stale assets');
+}
for (const asset of ['./index.html', './app.js', './app.css', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png']) {
if (!sw.includes(asset)) violations.push(`sw.js: precache entry missing for ${asset}`);
}
diff --git a/src/app.js b/src/app.js
index 0133cc3..19e3100 100644
--- a/src/app.js
+++ b/src/app.js
@@ -19,6 +19,9 @@ const translations = {
en: {
// Top bar
newFileBtn: 'New file',
+ examplesBtn: 'Examples',
+ examplesTitle: 'Load examples into a new notebook',
+ examplesFileName: 'examples.math',
// Modal
modalTitle: 'Create New File',
@@ -208,6 +211,9 @@ const translations = {
tr: {
// Top bar
newFileBtn: 'Yeni dosya',
+ examplesBtn: 'Örnekler',
+ examplesTitle: 'Örnekleri yeni bir not defterine yükle',
+ examplesFileName: 'ornekler.math',
// Modal
modalTitle: 'Yeni Dosya Oluştur',
@@ -418,6 +424,8 @@ function setLanguage(lang) {
function updateUILanguage() {
// Update tooltips
document.getElementById('new-tab-btn').title = t('newFileBtn');
+ document.getElementById('examples-btn').textContent = t('examplesBtn');
+ document.getElementById('examples-btn').title = t('examplesTitle');
document.getElementById('zoom-in').title = t('zoomIn');
document.getElementById('zoom-out').title = t('zoomOut');
@@ -658,6 +666,15 @@ function newFile(initialContent) {
input.select();
}
+function openExamplesNotebook() {
+ newFile(getDefaultContent());
+ const input = document.getElementById('new-file-input');
+ input.value = t('examplesFileName');
+ input.select();
+}
+
+document.getElementById('examples-btn').addEventListener('click', openExamplesNotebook);
+
function confirmNewFile() {
const input = document.getElementById('new-file-input');
const name = input.value.trim();
diff --git a/src/sw.js b/src/sw.js
index f3e3a55..d8de343 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -4,6 +4,45 @@ const PRECACHE_ASSETS = __PRECACHE_ASSETS__;
const APP_SHELL = new URL('./index.html', self.registration.scope).toString();
const scopedUrl = path => new URL(path, self.registration.scope).toString();
+const NETWORK_FIRST_PATHS = new Set(
+ ['./app.js', './app.css', './manifest.webmanifest'].map(
+ path => new URL(path, self.registration.scope).pathname
+ )
+);
+
+async function networkFirst(request, fallbackUrl = null) {
+ try {
+ const response = await fetch(request);
+ if (!response || !response.ok) throw new Error('Network response was not OK');
+
+ const cache = await caches.open(CACHE_NAME);
+ await cache.put(request, response.clone());
+ return response;
+ } catch {
+ const cache = await caches.open(CACHE_NAME);
+ const cached = await cache.match(request, { ignoreSearch: true });
+ if (cached) return cached;
+
+ if (fallbackUrl) {
+ const fallback = await cache.match(fallbackUrl);
+ if (fallback) return fallback;
+ }
+
+ return Response.error();
+ }
+}
+
+async function cacheFirst(request) {
+ const cache = await caches.open(CACHE_NAME);
+ const cached = await cache.match(request, { ignoreSearch: true });
+ if (cached) return cached;
+
+ const response = await fetch(request);
+ if (response && response.ok) {
+ await cache.put(request, response.clone());
+ }
+ return response;
+}
self.addEventListener('install', event => {
event.waitUntil(
@@ -37,17 +76,14 @@ self.addEventListener('fetch', event => {
if (url.origin !== self.location.origin) return;
if (request.mode === 'navigate') {
- event.respondWith(
- fetch(request)
- .then(response => response)
- .catch(() => caches.match(APP_SHELL))
- );
+ event.respondWith(networkFirst(request, APP_SHELL));
return;
}
- event.respondWith(
- caches
- .match(request, { ignoreSearch: true })
- .then(cached => cached || fetch(request))
- );
+ if (NETWORK_FIRST_PATHS.has(url.pathname)) {
+ event.respondWith(networkFirst(request));
+ return;
+ }
+
+ event.respondWith(cacheFirst(request));
});