diff options
Diffstat (limited to 'assets/js/visit-counter.js')
| -rw-r--r-- | assets/js/visit-counter.js | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/assets/js/visit-counter.js b/assets/js/visit-counter.js new file mode 100644 index 0000000..1715fdb --- /dev/null +++ b/assets/js/visit-counter.js @@ -0,0 +1,141 @@ +/* ============================================================================ + SillyLaird per-page visit counter (network-wide). + + Injected by nginx before </body> on HTML responses across *.sillylaird.ca + (see /etc/nginx/snippets/sillylaird-visit-counter.conf). Also safe to load + from site.js / nav.js. Runs once per page load (window.__slVisitCounterInit). + + Storage is localStorage + sessionStorage, keyed by host + path so each page + (and each subdomain) keeps its own count. Language variants share a key. + + Also beacons a privacy-friendly hit to /api/hit.php (no IP stored server-side; + only host/path, coarse browser·OS label, language, referrer host, local count). + ============================================================================ */ +(function () { + "use strict"; + + if (window.__slVisitCounterInit) return; + window.__slVisitCounterInit = true; + + var path = location.pathname || "/"; + // Skip admin / API / auth / binary endpoints — no counter chrome there. + if (/\/(admin|api|login|logout|auth|captcha|relay|meta|submit)(\/|$|\.)/i.test(path)) { + return; + } + if (/\.(xml|json|m3u|pls|css|js|map|ico|png|jpe?g|gif|webp|svg|woff2?|mp3|flac|webm|mp4)(\?|$)/i.test(path)) { + return; + } + + var HIT_URL = "https://www.sillylaird.ca/api/hit.php"; + + function pageKey() { + var p = (location.pathname || "/").replace(/\/+$/, "") || "/"; + // /foo/index.php and /foo/index_jp.php → /foo + p = p.replace(/\/index(?:_[a-z]{2})?\.php$/i, "") || "/"; + // /links_jp.php / links_zh.php → /links.php + p = p.replace(/_(jp|zh)\.php$/i, ".php"); + return location.hostname + p; + } + + function ensureStyles() { + if (document.getElementById("sl-visit-counter-style")) return; + var s = document.createElement("style"); + s.id = "sl-visit-counter-style"; + s.textContent = + ".site-visit-counter{margin:.5rem 0 0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;" + + "font-size:0.8rem;letter-spacing:0.08em;color:rgba(160,160,170,.9);}" + + ".site-visit-counter-digits{color:#39ff14;text-shadow:0 0 6px rgba(57,255,20,.35);}" + + "body.light-mode .site-visit-counter{color:rgba(40,40,50,.75);}" + + "body.light-mode .site-visit-counter-digits{color:#1a7a12;text-shadow:none;}"; + (document.head || document.documentElement).appendChild(s); + } + + function ensureMarkup() { + var digits = document.getElementById("site-visit-digits"); + if (digits) return digits; + + ensureStyles(); + + var wrap = document.createElement("p"); + wrap.className = "site-visit-counter"; + wrap.id = "site-visit-counter"; + wrap.setAttribute("aria-live", "polite"); + wrap.innerHTML = + '<span class="site-visit-counter-label">You are visitor</span> ' + + '<span class="site-visit-counter-digits" id="site-visit-digits">------</span>'; + + var footerInner = document.querySelector(".site-footer .footer-inner, footer .footer-inner"); + var footer = document.querySelector("footer.site-footer, footer"); + if (footerInner) { + footerInner.appendChild(wrap); + } else if (footer) { + footer.appendChild(wrap); + } else { + wrap.style.cssText = + "position:fixed;right:10px;bottom:8px;z-index:40;margin:0;padding:4px 8px;" + + "background:rgba(0,0,0,.55);border-radius:4px;backdrop-filter:blur(4px);"; + document.body.appendChild(wrap); + } + return document.getElementById("site-visit-digits"); + } + + function reportHit(localCount, isNew) { + var payload = { + host: location.hostname, + path: location.pathname || "/", + local_count: localCount, + unique: !!isNew, + lang: (navigator.language || "").slice(0, 16), + ref: document.referrer || "", + }; + var body = JSON.stringify(payload); + try { + if (navigator.sendBeacon) { + var blob = new Blob([body], { type: "application/json" }); + if (navigator.sendBeacon(HIT_URL, blob)) return; + } + } catch (_) {} + try { + fetch(HIT_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: body, + mode: "cors", + credentials: "omit", + keepalive: true, + }).catch(function () {}); + } catch (_) {} + } + + function count() { + var el = ensureMarkup(); + if (!el) return; + + var page = pageKey(); + var KEY = "sillylaird_visit_count_v2:" + page; + var SESS = "sillylaird_visit_sess_v2:" + page; + var n = 0; + var isNew = false; + try { + n = parseInt(localStorage.getItem(KEY) || "0", 10) || 0; + } catch (_) {} + try { + if (!sessionStorage.getItem(SESS)) { + n += 1; + isNew = true; + localStorage.setItem(KEY, String(n)); + sessionStorage.setItem(SESS, "1"); + } + } catch (_) {} + el.textContent = String(n).padStart(6, "0"); + + // Log every page view server-side (unique flag only when new session for page). + reportHit(n, isNew); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", count); + } else { + count(); + } +})(); |
