From 7c88bacad4a4d724c6e552be7fda4fd2d7932daa Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 19:04:51 +0530 Subject: [PATCH 1/6] feat: css defer --- templates/macros/head.html | 181 ++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 93 deletions(-) diff --git a/templates/macros/head.html b/templates/macros/head.html index d0dc871..70fb8b0 100644 --- a/templates/macros/head.html +++ b/templates/macros/head.html @@ -85,25 +85,26 @@ {{ load_data(path="static/js/core/boot.js") | safe }} - {# Font configuration with backward compatibility #} - {% if config.extra.font is defined %} - {% set font_enabled = config.extra.font.enabled | default(value=config.extra.custom_font_enabled | default(value=false)) %} - {% set font_name = config.extra.font.name | default(value=config.extra.custom_font_name | default(value='CustomFont')) %} - {% set font_path = config.extra.font.path | default(value=config.extra.custom_font_path | default(value='')) %} + {# Font configuration with backward compatibility #} + {% if config.extra.font is defined %} + {% set font_enabled = config.extra.font.enabled | default(value=config.extra.custom_font_enabled | default(value=false)) %} + {% set font_name = config.extra.font.name | default(value=config.extra.custom_font_name | default(value='CustomFont')) %} + {% set font_path = config.extra.font.path | default(value=config.extra.custom_font_path | default(value='')) %} + {% else %} + {% set font_enabled = config.extra.custom_font_enabled | default(value=false) %} + {% set font_name = config.extra.custom_font_name | default(value='CustomFont') %} + {% set font_path = config.extra.custom_font_path | default(value='') %} + {% endif %} + + {% if font_enabled and font_path != '' %} + {% if font_path is starting_with("http://") or font_path is starting_with("https://") %} + {% else %} - {% set font_enabled = config.extra.custom_font_enabled | default(value=false) %} - {% set font_name = config.extra.custom_font_name | default(value='CustomFont') %} - {% set font_path = config.extra.custom_font_path | default(value='') %} - {% endif %} - {% if font_enabled and font_path != '' %} - {% if font_path is starting_with("http://") or font_path is starting_with("https://") %} - - {% else %} {% set font_url = get_url(path=font_path) %} {% set font_css = "@font-face{font-family:'" ~ font_name ~ "';src:url('" ~ font_url ~ "') format('woff');font-weight:400;font-style:normal;font-display:swap;}body{font-family:'" ~ font_name ~ "',sans-serif !important;}" %} - - {% endif %} + {% endif %} + {% endif %} {% if meta_description != "" %} @@ -145,96 +146,90 @@ {% endif %} {{- resolved_title -}} - - {# Ahrefs Site Verification Integration via Env or Config #} - {% set env_ahrefs_key = get_env(name="AHREFS_KEY", default="") %} - {% if env_ahrefs_key != "" %} - - {% elif config.extra.ahrefs_analytics_key is defined and config.extra.ahrefs_analytics_key != "" %} - + {# Ahrefs Integration (Consolidated) #} + {% set ahrefs_key = get_env(name="AHREFS_KEY", default="") %} + {% if ahrefs_key == "" and config.extra.ahrefs_analytics_key is defined %} + {% set ahrefs_key = config.extra.ahrefs_analytics_key %} {% endif %} - {# Google Analytics and Sentry are loaded client-side only after cookie consent. #} - {# Ahrefs Analytics: lightweight site-analytics script, no consent gate required. #} - {% set env_ahrefs_key = get_env(name="AHREFS_KEY", default="") %} - {% if env_ahrefs_key != "" %} - {% set ahrefs_key = env_ahrefs_key %} - {% elif config.extra.ahrefs_analytics_key is defined and config.extra.ahrefs_analytics_key != "" %} - {% set ahrefs_key = config.extra.ahrefs_analytics_key %} - {% endif %} - {% if ahrefs_key is defined and ahrefs_key != "" %} - + function schedule(){ + if ("requestIdleCallback" in window) { + requestIdleCallback(load, { timeout: 3000 }); + } else { + setTimeout(load, 1500); + } + } + if (document.readyState === 'complete') schedule(); + else window.addEventListener('load', schedule, {once:true}); + })(); + {% endif %} - {{ macros_seo::meta_tags(config=config, current_url=current_url, resolved_title=resolved_title, resolved_description=resolved_description, resolved_meta_type=resolved_meta_type, page=page | default(value=false)) }} - {{ macros_seo::website_schema(config=config) }} + {{ macros_seo::meta_tags(config=config, current_url=current_url, resolved_title=resolved_title, resolved_description=resolved_description, resolved_meta_type=resolved_meta_type, page=page | default(value=false)) }} + {{ macros_seo::website_schema(config=config) }} - + - {% if config.generate_feeds %} - - {% endif %} + {% if config.generate_feeds %} + + {% endif %} - {# Favicon configuration #} - {# Default path is /icons/ for all favicon files #} - {# Users can override individual paths in [extra.favicon] section #} - {% if config.extra.favicon is defined %} - {% set favicon_base = config.extra.favicon.base_path | default(value="/icons/") %} - {% set favicon_16x16 = config.extra.favicon.favicon_16x16 | default(value=favicon_base ~ "favicon-16x16-transparent.png") %} - {% set favicon_32x32 = config.extra.favicon.favicon_32x32 | default(value=favicon_base ~ "favicon-32x32-transparent.png") %} - {% set favicon_96x96 = config.extra.favicon.favicon_96x96 | default(value=favicon_base ~ "favicon-96x96-transparent.png") %} - {% set favicon_svg = config.extra.favicon.favicon_svg | default(value=favicon_base ~ "favicon.svg") %} - {% set favicon_ico = config.extra.favicon.favicon_ico | default(value=favicon_base ~ "favicon-transparent.ico") %} - {% set apple_touch_icon = config.extra.favicon.apple_touch_icon | default(value=favicon_base ~ "apple-touch-icon-180x180-transparent.png") %} - {% set site_webmanifest = config.extra.favicon.site_webmanifest | default(value=favicon_base ~ "site.webmanifest") %} - {% else %} - {% set favicon_base = "/icons/" %} - {% set favicon_16x16 = favicon_base ~ "favicon-16x16-transparent.png" %} - {% set favicon_32x32 = favicon_base ~ "favicon-32x32-transparent.png" %} - {% set favicon_96x96 = favicon_base ~ "favicon-96x96-transparent.png" %} - {% set favicon_svg = favicon_base ~ "favicon.svg" %} - {% set favicon_ico = favicon_base ~ "favicon-transparent.ico" %} - {% set apple_touch_icon = favicon_base ~ "apple-touch-icon-180x180-transparent.png" %} - {% set site_webmanifest = favicon_base ~ "site.webmanifest" %} - {% endif %} - - - - - - - - - + {# Favicon configuration #} + {% if config.extra.favicon is defined %} + {% set favicon_base = config.extra.favicon.base_path | default(value="/icons/") %} + {% set favicon_16x16 = config.extra.favicon.favicon_16x16 | default(value=favicon_base ~ "favicon-16x16-transparent.png") %} + {% set favicon_32x32 = config.extra.favicon.favicon_32x32 | default(value=favicon_base ~ "favicon-32x32-transparent.png") %} + {% set favicon_96x96 = config.extra.favicon.favicon_96x96 | default(value=favicon_base ~ "favicon-96x96-transparent.png") %} + {% set favicon_svg = config.extra.favicon.favicon_svg | default(value=favicon_base ~ "favicon.svg") %} + {% set favicon_ico = config.extra.favicon.favicon_ico | default(value=favicon_base ~ "favicon-transparent.ico") %} + {% set apple_touch_icon = config.extra.favicon.apple_touch_icon | default(value=favicon_base ~ "apple-touch-icon-180x180-transparent.png") %} + {% set site_webmanifest = config.extra.favicon.site_webmanifest | default(value=favicon_base ~ "site.webmanifest") %} + {% else %} + {% set favicon_base = "/icons/" %} + {% set favicon_16x16 = favicon_base ~ "favicon-16x16-transparent.png" %} + {% set favicon_32x32 = favicon_base ~ "favicon-32x32-transparent.png" %} + {% set favicon_96x96 = favicon_base ~ "favicon-96x96-transparent.png" %} + {% set favicon_svg = favicon_base ~ "favicon.svg" %} + {% set favicon_ico = favicon_base ~ "favicon-transparent.ico" %} + {% set apple_touch_icon = favicon_base ~ "apple-touch-icon-180x180-transparent.png" %} + {% set site_webmanifest = favicon_base ~ "site.webmanifest" %} + {% endif %} + + + + + + + + + - - + + + + + + data-katex-css="{{ get_url(path="css/katex.min.css") | safe }}" + data-sw-path="{{ get_url(path="sw.js") | safe }}"> {% endmacro head %} \ No newline at end of file From 9c7bd1be2e7d6068cb5d0e837f18bbab548f6f74 Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 19:34:17 +0530 Subject: [PATCH 2/6] fix: offline template --- static/js/system/offline-reload.js | 12 ++++++++++++ templates/offline.html | 27 ++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/static/js/system/offline-reload.js b/static/js/system/offline-reload.js index 8466f75..31fada9 100644 --- a/static/js/system/offline-reload.js +++ b/static/js/system/offline-reload.js @@ -6,7 +6,9 @@ if (!el) return; var ATTEMPT_KEY = "offlineReloadLastAttempt"; var ATTEMPT_WINDOW_MS = 6000; + var POLL_INTERVAL_MS = 5000; var isChecking = false; + var pollInterval = null; el.textContent = "Waiting for connection"; el.classList.add("status-pill", "status-pill--pending"); @@ -16,6 +18,8 @@ el.classList.remove("status-pill--pending"); el.classList.add("status-pill--online"); + if (pollInterval) clearInterval(pollInterval); + setTimeout(function () { var path = window.location.pathname || "/"; if (path === "/offline" || path === "/offline/") { @@ -84,4 +88,12 @@ } window.addEventListener("online", maybeRecover); + + // Periodic polling: keep checking network status every POLL_INTERVAL_MS + // even if 'online' event doesn't fire. This ensures recovery on reconnect. + pollInterval = setInterval(function () { + if (navigator.onLine) { + maybeRecover(); + } + }, POLL_INTERVAL_MS); })(); diff --git a/templates/offline.html b/templates/offline.html index 6a43451..03cc08c 100644 --- a/templates/offline.html +++ b/templates/offline.html @@ -1,12 +1,33 @@ -{% extends "page.html" %} +{% extends "base.html" %} {% import "macros/nav.html" as macros_nav %} +{# Offline page — extends base.html directly (like 404.html) to force the rail layout. + This ensures _force_rail = true, which sets use_fit_shell = true for consistent width. #} + +{# Force the shell into the fitted layout so offline matches site width/state #} +{% set shell_fit = true %} + +{% block extra_head %} + +{% endblock extra_head %} + +{% block content %} +
+
+

Offline

+

You appear to be offline.

+
+
+ +

Your browser has lost connection to the internet. Check your network and try reloading the page.

+

Waiting for connection

+{% endblock content %} + {% block prev_link %} {{ macros_nav::render_button(url="/", title="Home", subtitle="Back to", direction="prev") }} {% endblock prev_link %} -{% block next_link %} -{% endblock next_link %} +{% block next_link %}{% endblock next_link %} {% block js_body %} {{ super() }} From 7795bb76a1a8243ccb81bbb085ff222fbd399db0 Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 19:37:14 +0530 Subject: [PATCH 3/6] fix: scripts errors --- scripts/clean-pagination-redirects.js | 7 ++++++- scripts/sync-project-data.js | 30 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/scripts/clean-pagination-redirects.js b/scripts/clean-pagination-redirects.js index 230919c..0cf2b63 100644 --- a/scripts/clean-pagination-redirects.js +++ b/scripts/clean-pagination-redirects.js @@ -66,7 +66,12 @@ if (fs.existsSync(sitemapPath) && removedPaths.length > 0) { } } if (purged > 0) { - fs.writeFileSync(sitemapPath, sitemap, "utf8"); + // Write atomically to avoid race conditions: write to a temp file + // and rename into place. This prevents other processes from reading + // a partially-written file and addresses file-system race warnings. + const tmpPath = sitemapPath + ".tmp"; + fs.writeFileSync(tmpPath, sitemap, "utf8"); + fs.renameSync(tmpPath, sitemapPath); console.log(`Purged ${purged} stale page/1 URL(s) from sitemap.xml.`); } } diff --git a/scripts/sync-project-data.js b/scripts/sync-project-data.js index 0512b60..40aeb56 100644 --- a/scripts/sync-project-data.js +++ b/scripts/sync-project-data.js @@ -28,10 +28,16 @@ function parseRepoFromUrl(url) { const match = value.match(/github\.com\/([^/]+)\/([^/#?]+)/i); if (!match) return null; - return { - owner: match[1], - repo: match[2].replace(/\.git$/i, ""), - }; + const owner = match[1]; + const repo = match[2].replace(/\.git$/i, ""); + + // Validate owner and repo against a strict whitelist of characters to + // prevent crafted file data from being used to build unexpected URLs. + // GitHub owners: alphanumeric and hyphens; repos: allow dot/underscore/hyphen. + if (!/^[A-Za-z0-9-]+$/.test(owner)) return null; + if (!/^[A-Za-z0-9._-]+$/.test(repo)) return null; + + return { owner, repo }; } function getGitHubToken() { @@ -60,8 +66,22 @@ function githubRequest(endpoint, token) { if (token) headers.Authorization = `Bearer ${token}`; // lgtm[js/file-access-to-http] token sourced from env or gh CLI return new Promise((resolve, reject) => { + // Construct the full URL via the URL API and validate the hostname so + // injected or unexpected endpoint values cannot cause requests to other + // origins. This ensures outbound requests are constrained to GitHub. + let urlObj; + try { + urlObj = new URL(endpoint, API_BASE); + } catch (err) { + return reject(new Error("Invalid GitHub API endpoint")); + } + + if (urlObj.hostname !== new URL(API_BASE).hostname) { + return reject(new Error("Disallowed GitHub API host")); + } + const req = https.request( - `${API_BASE}${endpoint}`, + urlObj, { method: "GET", headers, From fd7e663d2a387ed6e71845e210b5b26a9d0ab0f0 Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 19:43:25 +0530 Subject: [PATCH 4/6] fix: hints wrong in toc --- static/js/features/access-keys.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/static/js/features/access-keys.js b/static/js/features/access-keys.js index d84523c..858d57d 100644 --- a/static/js/features/access-keys.js +++ b/static/js/features/access-keys.js @@ -371,9 +371,31 @@ function showHints() { for (const el of targets) { let explicitHint = null; - // Semantic rules — skip TOC sidebar: those are page-internal anchors - // (#skills, #section-heading) that must not inherit site-nav shortcuts. - if (!el.closest("[data-toc-sidebar]")) { + // Semantic rules — normally skip TOC sidebar because those are + // page-internal anchors that must not inherit site-nav shortcuts. + // However, some TOC entries are site-level links (e.g. /archive/, /about/) + // and should still receive their semantic shortcuts. Detect those by + // allowing semantic-rule matching for anchors that point to a same-origin + // pathname with no hash (i.e. true site nav links), even when inside the + // TOC sidebar. + const inToc = !!el.closest("[data-toc-sidebar]"); + let allowSemanticInToc = false; + if (el.tagName === "A" && el.href) { + try { + const u = new URL(el.href, window.location.origin); + // same-origin and no fragment/hash + if (u.origin === window.location.origin && !u.hash) { + // treat shallow site nav paths as nav links (e.g. /about/, /archive/) + const segments = u.pathname.replace(/(^\/|\/$)/g, "").split("/"); + // allow 0 (root) or 1-segment paths to be treated as site links + if (segments.length <= 1) allowSemanticInToc = true; + } + } catch (e) { + allowSemanticInToc = false; + } + } + + if (!inToc || allowSemanticInToc) { for (const rule of SEMANTIC_RULES) { if ( (rule.selector && el.matches(rule.selector)) || From 42ea8a1c89d2b017e10306666b698811300bd0ce Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 19:47:02 +0530 Subject: [PATCH 5/6] fix: dark mode logo flicker-> made sync --- scripts/sync-project-data.js | 2 +- static/js/core/boot.js | 10 ++++++++++ static/js/features/access-keys.js | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/sync-project-data.js b/scripts/sync-project-data.js index 40aeb56..d45ea32 100644 --- a/scripts/sync-project-data.js +++ b/scripts/sync-project-data.js @@ -72,7 +72,7 @@ function githubRequest(endpoint, token) { let urlObj; try { urlObj = new URL(endpoint, API_BASE); - } catch (err) { + } catch { return reject(new Error("Invalid GitHub API endpoint")); } diff --git a/static/js/core/boot.js b/static/js/core/boot.js index b16a274..78865db 100644 --- a/static/js/core/boot.js +++ b/static/js/core/boot.js @@ -24,6 +24,16 @@ : raw; d.setAttribute("data-theme", mode); + // Also set a theme class immediately so CSS prepaint rules using + // `html.dark` / `html.light` apply before the JS modules load. This + // prevents a flash/flicker of the wrong logo variant during navigation. + try { + d.classList.remove("light", "dark"); + d.classList.add(mode); + } catch { + // Ignore failures (e.g. if classList isn't supported). The data-theme + // attribute will still be set correctly. + } d.setAttribute("data-ui-init", "0"); d.setAttribute( "data-sidebar-collapsed", diff --git a/static/js/features/access-keys.js b/static/js/features/access-keys.js index 858d57d..e19237d 100644 --- a/static/js/features/access-keys.js +++ b/static/js/features/access-keys.js @@ -390,7 +390,7 @@ function showHints() { // allow 0 (root) or 1-segment paths to be treated as site links if (segments.length <= 1) allowSemanticInToc = true; } - } catch (e) { + } catch { allowSemanticInToc = false; } } From 8385a3fbc04c881c65b29ed3d4bbe772b17c9c4b Mon Sep 17 00:00:00 2001 From: kascit Date: Mon, 11 May 2026 20:05:26 +0530 Subject: [PATCH 6/6] fix: copilot finds --- LLM_RULES.md | 4 ++-- static/js/core/boot.js | 16 ++++++++++++++++ static/js/features/access-keys.js | 29 ++++------------------------- static/js/system/offline-reload.js | 20 ++++++++++++++++++++ templates/base.html | 2 +- templates/macros/head.html | 6 ++---- 6 files changed, 45 insertions(+), 32 deletions(-) diff --git a/LLM_RULES.md b/LLM_RULES.md index 663a383..7328b2f 100644 --- a/LLM_RULES.md +++ b/LLM_RULES.md @@ -52,7 +52,7 @@ selectors: data-\*-mount ONLY for JS mount points. no generic IDs except well-kn responsive: use static/js/core/responsive.js helpers. no ad-hoc breakpoints in JS. daisyUI: data-tip XOR title. never both. typography: HTML class="not-prose". NO CSS @apply not-prose. -access-keys: SEMANTIC_RULES in access-keys.js = single source of truth for keyboard hints. no data-hint in templates except override cases. elements inside [data-toc-sidebar] are excluded from semantic rules (internal anchors). +access-keys: SEMANTIC_RULES in access-keys.js = single source of truth for keyboard hints. no data-hint in templates except override cases. elements inside [data-toc-sidebar] are excluded from semantic rules (internal anchors). if a force-rail page needs a shortcut for a rail item like Archive, set an explicit `data-hint` on that template link. TOC: single initToc() from toc.js covers ALL pages (blog, archive, taxonomy). getTocLink uses href$=#ID for full-URL zola children AND data-toc-ID for summary parents AND bare #ID for archive anchors. notify-banner: path is js/ui/notify-banner.js (not root-level js/). @@ -62,7 +62,7 @@ macros: import separate files. no deep same-file chaining. 404 routing: templates/404.html ONLY. no content/404.md. offline page: templates/offline.html (no content/offline.md). 404 + offline layout: \_force_rail=true in base.html → use_fit_shell=true + has_layout_rail=true + empty #toc-sidebar nav (aria-label="Page tools") for full structural parity. Toggle button label is "Hide/Show tools rail" (not "table of contents"). offline page must have force_rail=true in [extra]. -CSP: no inline style="" attributes. use CSS classes. JS DOM manipulation toggles classes only. +CSP: no inline style="" attributes. use CSS classes. JS DOM manipulation toggles classes only. deferred style sheets use `data-defer-css="true"` and get activated by `static/js/core/boot.js`; avoid inline event-handler attributes on style sheet links. SEO / SCHEMA article JSON-LD: author Person must include both name AND URL (→ /about/). diff --git a/static/js/core/boot.js b/static/js/core/boot.js index 78865db..d4909de 100644 --- a/static/js/core/boot.js +++ b/static/js/core/boot.js @@ -40,4 +40,20 @@ getL("sidebar-collapsed") ? "1" : "0", ); d.setAttribute("data-toc-collapsed", getL("toc-collapsed") ? "1" : "0"); + + function activateDeferredCss() { + var links = document.querySelectorAll('link[data-defer-css="true"]'); + for (var i = 0; i < links.length; i += 1) { + links[i].media = "all"; + links[i].removeAttribute("data-defer-css"); + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", activateDeferredCss, { + once: true, + }); + } else { + activateDeferredCss(); + } })(); diff --git a/static/js/features/access-keys.js b/static/js/features/access-keys.js index e19237d..3cf50fa 100644 --- a/static/js/features/access-keys.js +++ b/static/js/features/access-keys.js @@ -371,31 +371,10 @@ function showHints() { for (const el of targets) { let explicitHint = null; - // Semantic rules — normally skip TOC sidebar because those are - // page-internal anchors that must not inherit site-nav shortcuts. - // However, some TOC entries are site-level links (e.g. /archive/, /about/) - // and should still receive their semantic shortcuts. Detect those by - // allowing semantic-rule matching for anchors that point to a same-origin - // pathname with no hash (i.e. true site nav links), even when inside the - // TOC sidebar. - const inToc = !!el.closest("[data-toc-sidebar]"); - let allowSemanticInToc = false; - if (el.tagName === "A" && el.href) { - try { - const u = new URL(el.href, window.location.origin); - // same-origin and no fragment/hash - if (u.origin === window.location.origin && !u.hash) { - // treat shallow site nav paths as nav links (e.g. /about/, /archive/) - const segments = u.pathname.replace(/(^\/|\/$)/g, "").split("/"); - // allow 0 (root) or 1-segment paths to be treated as site links - if (segments.length <= 1) allowSemanticInToc = true; - } - } catch { - allowSemanticInToc = false; - } - } - - if (!inToc || allowSemanticInToc) { + // Semantic rules — skip TOC sidebar: those are page-internal anchors that + // must not inherit site-nav shortcuts. If a TOC entry needs a shortcut, + // it should opt in explicitly with data-hint in the template. + if (!el.closest("[data-toc-sidebar]")) { for (const rule of SEMANTIC_RULES) { if ( (rule.selector && el.matches(rule.selector)) || diff --git a/static/js/system/offline-reload.js b/static/js/system/offline-reload.js index 31fada9..5cb0f1f 100644 --- a/static/js/system/offline-reload.js +++ b/static/js/system/offline-reload.js @@ -7,6 +7,7 @@ var ATTEMPT_KEY = "offlineReloadLastAttempt"; var ATTEMPT_WINDOW_MS = 6000; var POLL_INTERVAL_MS = 5000; + var PING_TIMEOUT_MS = 2500; var isChecking = false; var pollInterval = null; @@ -49,16 +50,25 @@ function probeNetwork() { // Requests under /__runtime/ are excluded from SW handling in static/sw.js. + var controller = new AbortController(); + var timeoutId = window.setTimeout(function () { + controller.abort(); + }, PING_TIMEOUT_MS); + return fetch("/__runtime/ping?ts=" + Date.now(), { method: "GET", cache: "no-store", credentials: "same-origin", + signal: controller.signal, }) .then(function () { return true; }) .catch(function () { return false; + }) + .finally(function () { + window.clearTimeout(timeoutId); }); } @@ -89,6 +99,13 @@ window.addEventListener("online", maybeRecover); + function stopPolling() { + if (pollInterval) { + window.clearInterval(pollInterval); + pollInterval = null; + } + } + // Periodic polling: keep checking network status every POLL_INTERVAL_MS // even if 'online' event doesn't fire. This ensures recovery on reconnect. pollInterval = setInterval(function () { @@ -96,4 +113,7 @@ maybeRecover(); } }, POLL_INTERVAL_MS); + + window.addEventListener("pagehide", stopPolling, { once: true }); + window.addEventListener("beforeunload", stopPolling, { once: true }); })(); diff --git a/templates/base.html b/templates/base.html index d4504e4..dca20a0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -255,7 +255,7 @@

Blog
  • Links
  • {% endif %} -
  • +
  • Archive
  • diff --git a/templates/macros/head.html b/templates/macros/head.html index 70fb8b0..6123aad 100644 --- a/templates/macros/head.html +++ b/templates/macros/head.html @@ -219,10 +219,8 @@ - - - - + +