aboutsummaryrefslogtreecommitdiffstats
path: root/assets/js/visit-counter.js
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /assets/js/visit-counter.js
downloadwww-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.tar.gz
www-898b52edcb47bcb3e9d6106e74ca73e74ea01e70.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to 'assets/js/visit-counter.js')
-rw-r--r--assets/js/visit-counter.js141
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();
+ }
+})();