site: first-party, cookieless visit stats (self-hosted Umami at storebound.dev/s/, Do Not Track respected) with events for copy buttons, desktop downloads per OS, the teaser (play, 50 %, complete) and outbound docs / git / npm links; a privacy note in the footer; docs/site-analytics.md and tools/vds/ configs to rebuild the server side; scrub allows a page's public stats website id

This commit is contained in:
CRTkafa 2026-10-14 11:11:13 +08:00
parent 83c14d3a8e
commit 34c2fa22e5
7 changed files with 129 additions and 3 deletions

View file

@ -1,5 +1,28 @@
// storebound.dev: copy buttons, tabs (AI tools, desktop screens), the teaser player, the demo (real CLI output), the
// How it works stepper and the desktop downloads (reads desktop/latest.json, same origin). No storage, no third parties.
// Stats: self-hosted Umami at /s/ (cookieless, Do Not Track respected); ev() is a no-op when the script is blocked or off.
(() => {
const ev = (name, data) => { try { window.umami?.track(name, data); } catch {} };
document.addEventListener('click', e => {
const a = e.target.closest?.('a[href]');
if (!a) return;
if (a.dataset.dl) { ev('desktop-download', { os: a.dataset.dl.split('-')[0], file: a.dataset.dl }); return; }
let u; try { u = new URL(a.href, location.href); } catch { return; }
if (u.host === location.host) return;
const docs = u.host === 'git.crt.fyi' && /\/(docs\/|README|CHANGELOG|SECURITY)/.test(u.pathname);
const kind = docs ? 'docs' : u.host === 'git.crt.fyi' ? 'git' : u.host.endsWith('npmjs.com') ? 'npm' : 'other';
ev('outbound', { kind, host: u.host, path: u.pathname.slice(0, 120) });
}, true);
const vid = document.querySelector('#teaser video');
if (vid) {
const seen = new Set();
const once = (k, d) => { if (!seen.has(k)) { seen.add(k); ev(k, d); } };
vid.addEventListener('play', () => once('teaser-play'));
vid.addEventListener('timeupdate', () => { if (vid.duration && vid.currentTime / vid.duration >= 0.5) once('teaser-50'); });
vid.addEventListener('ended', () => once('teaser-complete'));
}
window.__sbEv = ev;
})();
(() => {
const copyText = async text => {
try { await navigator.clipboard.writeText(text); return true; } catch {}
@ -15,6 +38,7 @@
// Drop "# comment" tails so the copied commands run as is.
const text = btn.dataset.copy ?? (target ? target.innerText.split('\n').map(l => l.replace(/\s+#.*$/, '')).join('\n').trim() : '');
const ok = await copyText(text);
window.__sbEv?.('copy', { what: (btn.dataset.copy ?? btn.dataset.copyTarget ?? '').slice(0, 80) });
btn.textContent = ok ? 'Copied' : 'Press Ctrl+C';
btn.classList.toggle('done', ok);
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('done'); }, 1600);