Analytics: Banner-Skript und Heatmap-Overlay

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 09:50:20 +00:00
1 parent 305939fdbd
commit 5930f052d7
7 files changed
+1379

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
// Erzeugt dist/w.js und dist/h.js: core.js + Einstieg als IIFE, ohne import/export (kein Build-Tool).
// Aufruf: node client/build.js
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const DIR = dirname(fileURLToPath(import.meta.url));
const read = (f) => readFileSync(join(DIR, f), 'utf8');
function wrap(name, entry) {
const core = read('core.js').replace(/^export\s+/gm, '');
const body = read(entry).replace(/^import\s[^\n]*\n/gm, '');
return `/* WEBklar Analyse ${name} - generiert aus client/core.js + client/${entry} (node client/build.js), nicht von Hand aendern */\n(function () {\n'use strict';\n${core}\n${body}\n})();\n`;
}
export function buildAll() {
return { w: wrap('w.js', 'w.js'), h: wrap('h.js', 'h.js') };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const b = buildAll();
mkdirSync(join(DIR, 'dist'), { recursive: true });
writeFileSync(join(DIR, 'dist', 'w.js'), b.w);
writeFileSync(join(DIR, 'dist', 'h.js'), b.h);
console.log('dist/w.js', b.w.length, 'Bytes; dist/h.js', b.h.length, 'Bytes');
}
+120
View File
@@ -0,0 +1,120 @@
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
export const CONSENT_KEY = 'wk-consent';
export const CONSENT_MAX_AGE = 183 * 86400000;
export function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
export function cleanPath(href) {
let p = '/';
try {
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
} catch (e) { /* bleibt '/' */ }
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
export function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
export function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 5) {
const tag = String(n.tagName).toLowerCase();
let seg = tag;
const id = n.id ? safeName(n.id) : '';
if (id && !looksGenerated(id)) {
parts.unshift(seg + '#' + id);
break;
}
const raw = n.getAttribute ? n.getAttribute('class') : '';
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
export function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
export function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
export function readConsent(storage, now) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t };
} catch (e) { return null; }
}
export function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
export function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
+224
View File
@@ -0,0 +1,224 @@
/* WEBklar Analyse h.js - generiert aus client/core.js + client/h.js (node client/build.js), nicht von Hand aendern */
(function () {
'use strict';
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
function cleanPath(href) {
let p = '/';
try {
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
} catch (e) { /* bleibt '/' */ }
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 5) {
const tag = String(n.tagName).toLowerCase();
let seg = tag;
const id = n.id ? safeName(n.id) : '';
if (id && !looksGenerated(id)) {
parts.unshift(seg + '#' + id);
break;
}
const raw = n.getAttribute ? n.getAttribute('class') : '';
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
function readConsent(storage, now) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t };
} catch (e) { return null; }
}
function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
function main() {
const me = document.currentScript;
const token = me && me.getAttribute('data-token');
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
if (!token || !base) return;
const de = document.documentElement;
const host = document.createElement('div');
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
const root = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
const bar = document.createElement('div');
bar.className = 'bar';
const msg = document.createElement('span');
msg.innerHTML = escapeHtml('Heatmap wird geladen ...');
const close = document.createElement('button');
close.setAttribute('type', 'button');
close.textContent = 'Schließen';
bar.appendChild(msg); bar.appendChild(close);
root.appendChild(style); root.appendChild(bar);
document.body.appendChild(host);
const canvas = document.createElement('canvas');
const cs = canvas.style;
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
document.body.appendChild(canvas);
let points = [];
let lastW = 0, lastH = 0, timer = 0;
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
const ramp = (function () {
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
const g = c.getContext('2d');
const lg = g.createLinearGradient(0, 0, 256, 0);
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
return g.getImageData(0, 0, 256, 1).data;
})();
function draw() {
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
lastW = w; lastH = h;
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
canvas.width = cw; canvas.height = ch;
cs.width = w + 'px'; cs.height = h + 'px';
const g = canvas.getContext('2d');
g.clearRect(0, 0, cw, ch);
const r = Math.max(6, 30 * scale);
for (const p of points) {
const x = p.x * cw, y = p.y * ch;
const rg = g.createRadialGradient(x, y, 0, x, y, r);
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
}
// Deckkraft-Stufen in Farben umsetzen
const img = g.getImageData(0, 0, cw, ch);
const d = img.data;
for (let i = 3; i < d.length; i += 4) {
const a = d[i];
if (!a) continue;
const k = Math.min(255, a * 3) * 4;
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
d[i] = Math.min(200, 60 + a);
}
g.putImageData(img, 0, 0);
}
function redrawIfChanged() {
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
}
function stop() {
clearInterval(timer);
window.removeEventListener('resize', draw);
host.remove(); canvas.remove();
}
close.addEventListener('click', stop);
window.addEventListener('resize', draw);
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)');
draw();
timer = setInterval(redrawIfChanged, 1000);
})
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); });
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
})();
+345
View File
@@ -0,0 +1,345 @@
/* WEBklar Analyse w.js - generiert aus client/core.js + client/w.js (node client/build.js), nicht von Hand aendern */
(function () {
'use strict';
// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten).
// Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen.
const CONSENT_KEY = 'wk-consent';
const CONSENT_MAX_AGE = 183 * 86400000;
function vpClass(width) {
const w = Number(width);
if (w < 768) return 'm';
if (w < 1024) return 't';
return 'd';
}
function cleanPath(href) {
let p = '/';
try {
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
} catch (e) { /* bleibt '/' */ }
try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ }
if (p.charAt(0) !== '/') p = '/' + p;
return p.slice(0, 300);
}
const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, '');
function refHost(referrer, ownHost) {
if (!referrer) return '';
try {
const u = new URL(String(referrer));
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
const h = u.hostname.toLowerCase();
if (!h || stripWww(h) === stripWww(ownHost || '')) return '';
return h;
} catch (e) { return ''; }
}
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
function looksGenerated(s) {
if (/\d{4,}/.test(s)) return true;
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
return s.split(/[-_]/).some((p) => mixed(p, 6));
}
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
function cssPath(el) {
const parts = [];
let n = el;
while (n && n.tagName && parts.length < 5) {
const tag = String(n.tagName).toLowerCase();
let seg = tag;
const id = n.id ? safeName(n.id) : '';
if (id && !looksGenerated(id)) {
parts.unshift(seg + '#' + id);
break;
}
const raw = n.getAttribute ? n.getAttribute('class') : '';
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
if (cls.length) seg += '.' + cls.join('.');
parts.unshift(seg);
n = n.parentNode;
}
let out = parts.join(' > ');
while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); }
return out.slice(0, 200);
}
function parseHref(a) {
const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || '';
return String(href).trim();
}
function linkHost(a) {
try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; }
}
function contactKind(a, ownHost) {
const href = parseHref(a);
if (!href) return null;
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href);
if (!scheme) return null; // relativ, Anker
const s = scheme[1].toLowerCase();
if (s === 'tel') return 'tel';
if (s === 'mailto') return 'mail';
if (s === 'whatsapp') return 'wa';
if (s !== 'http' && s !== 'https') return null;
const host = linkHost(a);
if (!host) return null;
if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa';
const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost;
return stripWww(host) === stripWww(own || '') ? null : 'out';
}
function readConsent(storage, now) {
try {
const raw = storage.getItem(CONSENT_KEY);
if (!raw) return null;
const c = JSON.parse(raw);
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
const age = now - c.t;
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
return { v: 1, a: c.a, t: c.t };
} catch (e) { return null; }
}
function contrastOk(hex) {
if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false;
const lin = (i) => {
const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2);
return 1.05 / (L + 0.05) >= 4.5;
}
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
// Keine Cookies, keine Kennung im Browser. Nur pv ohne Einwilligung; ck/ct/en nur nach Annehmen.
function main() {
const DOC = document;
const WIN = window;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src$="/a/w.js"]');
if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
// ---- Heatmap-Modus: nichts senden, kein Banner ----
const qs = new URLSearchParams(WIN.location.search);
const heatToken = qs.get('__wk_heat');
if (heatToken) {
qs.delete('__wk_heat');
const rest = qs.toString();
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
const s = DOC.createElement('script');
s.setAttribute('src', base + 'h.js');
s.setAttribute('data-token', heatToken);
(DOC.head || DOC.documentElement).appendChild(s);
return;
}
// ---- Senden: Fehler werden verschluckt ----
function send(obj) {
try {
const body = JSON.stringify(obj);
if (navigator.sendBeacon && navigator.sendBeacon(endpoint, body)) return;
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
} catch (e) { /* egal */ }
}
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
const vp = () => vpClass(WIN.innerWidth);
const ownHost = () => WIN.location.hostname;
// ---- Seitenaufruf (pv): immer, auch ohne Einwilligung ----
let lastPath = null;
let pvStart = Date.now();
let maxScroll = 0;
let enSent = false;
function sendPv(first) {
lastPath = cleanPath(WIN.location.pathname);
pvStart = Date.now(); maxScroll = 0; enSent = false;
send({ t: 'pv', site, path: lastPath, ref: first ? refHost(DOC.referrer, ownHost()) : '', vp: vp() });
}
let navTimer = null;
function scheduleNav() {
if (navTimer) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
}), 300);
}
sendPv(true);
try {
const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
WIN.addEventListener('popstate', safe(scheduleNav));
} catch (e) { /* egal */ }
// ---- Tracking (nur nach Annehmen erreichbar) ----
let tracking = false;
const hosts = [];
const scrollPct = () => {
const h = DOC.documentElement.scrollHeight || 1;
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
};
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) {
const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
const path = cleanPath(WIN.location.pathname);
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
let a = t;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
const kind = a ? contactKind(a, ownHost()) : null;
if (kind) {
const ev = { t: 'ct', site, path, kind };
if (kind === 'out') ev.host = linkHost(a);
send(ev);
}
});
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
const onLeave = safe(function () {
if (enSent) return;
enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
});
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
function startTracking() {
if (tracking) return;
tracking = true;
DOC.addEventListener('click', onClick, { capture: true, passive: true });
DOC.addEventListener('visibilitychange', onVis);
WIN.addEventListener('pagehide', onLeave);
WIN.addEventListener('scroll', onScroll, { passive: true });
}
function stopTracking() {
tracking = false;
DOC.removeEventListener('click', onClick, { capture: true });
DOC.removeEventListener('visibilitychange', onVis);
WIN.removeEventListener('pagehide', onLeave);
WIN.removeEventListener('scroll', onScroll);
}
// ---- Einwilligung ----
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1';
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const stored = readConsent(storage, Date.now());
let bannerHost = null;
let linkHostEl = null;
function choose(a) {
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') startTracking(); else stopTracking();
closeBanner();
}
function el(tag, attrs, text) {
const n = DOC.createElement(tag);
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
if (text != null) n.textContent = text;
return n;
}
const CSS = ':host{all:initial}' +
'*{box-sizing:border-box}' +
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
'.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' +
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
function openBanner() {
if (bannerHost) return;
const prev = DOC.activeElement;
bannerHost = DOC.createElement('div');
const st = bannerHost.style;
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
hosts.push(bannerHost);
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der ');
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info'));
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes);
bar.appendChild(p); bar.appendChild(row);
root.appendChild(bar);
DOC.body.appendChild(bannerHost);
if (linkHostEl) linkHostEl.style.display = 'none';
no.focus();
}
function closeBanner() {
if (!bannerHost) return;
const i = hosts.indexOf(bannerHost);
if (i !== -1) hosts.splice(i, 1);
bannerHost.remove();
bannerHost = null;
if (linkHostEl) linkHostEl.style.display = '';
}
function addSettingsLink() {
linkHostEl = DOC.createElement('div');
const st = linkHostEl.style;
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
hosts.push(linkHostEl);
const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
b.addEventListener('click', safe(openBanner));
root.appendChild(b);
DOC.body.appendChild(linkHostEl);
}
if (DOC.querySelector('[data-webklar-consent]')) {
DOC.addEventListener('click', safe(function (e) {
const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(); }
}));
} else {
addSettingsLink();
}
if (stored) {
if (stored.a === 'accept') startTracking();
} else {
openBanner();
send({ t: 'bn', site, a: 'shown' });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
})();
+99
View File
@@ -0,0 +1,99 @@
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
import { escapeHtml } from './core.js';
function main() {
const me = document.currentScript;
const token = me && me.getAttribute('data-token');
const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
if (!token || !base) return;
const de = document.documentElement;
const host = document.createElement('div');
host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647';
const root = host.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}';
const bar = document.createElement('div');
bar.className = 'bar';
const msg = document.createElement('span');
msg.innerHTML = escapeHtml('Heatmap wird geladen ...');
const close = document.createElement('button');
close.setAttribute('type', 'button');
close.textContent = 'Schließen';
bar.appendChild(msg); bar.appendChild(close);
root.appendChild(style); root.appendChild(bar);
document.body.appendChild(host);
const canvas = document.createElement('canvas');
const cs = canvas.style;
cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646';
document.body.appendChild(canvas);
let points = [];
let lastW = 0, lastH = 0, timer = 0;
// Farbverlauf: transparent -> blau -> gruen -> gelb -> rot
const ramp = (function () {
const c = document.createElement('canvas'); c.width = 256; c.height = 1;
const g = c.getContext('2d');
const lg = g.createLinearGradient(0, 0, 256, 0);
lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)');
g.fillStyle = lg; g.fillRect(0, 0, 256, 1);
return g.getImageData(0, 0, 256, 1).data;
})();
function draw() {
const w = Math.max(de.scrollWidth, de.clientWidth, 1);
const h = Math.max(de.scrollHeight, de.clientHeight, 1);
lastW = w; lastH = h;
// Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel
const scale = Math.min(1, Math.sqrt(4e6 / (w * h)));
const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale));
canvas.width = cw; canvas.height = ch;
cs.width = w + 'px'; cs.height = h + 'px';
const g = canvas.getContext('2d');
g.clearRect(0, 0, cw, ch);
const r = Math.max(6, 30 * scale);
for (const p of points) {
const x = p.x * cw, y = p.y * ch;
const rg = g.createRadialGradient(x, y, 0, x, y, r);
rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2);
}
// Deckkraft-Stufen in Farben umsetzen
const img = g.getImageData(0, 0, cw, ch);
const d = img.data;
for (let i = 3; i < d.length; i += 4) {
const a = d[i];
if (!a) continue;
const k = Math.min(255, a * 3) * 4;
d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2];
d[i] = Math.min(200, 60 + a);
}
g.putImageData(img, 0, 0);
}
function redrawIfChanged() {
if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw();
}
function stop() {
clearInterval(timer);
window.removeEventListener('resize', draw);
host.remove(); canvas.remove();
}
close.addEventListener('click', stop);
window.addEventListener('resize', draw);
fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' })
.then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); })
.then(function (j) {
points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; });
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)');
draw();
timer = setInterval(redrawIfChanged, 1000);
})
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); });
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }
+220
View File
@@ -0,0 +1,220 @@
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
// Keine Cookies, keine Kennung im Browser. Nur pv ohne Einwilligung; ck/ct/en nur nach Annehmen.
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, CONSENT_KEY } from './core.js';
function main() {
const DOC = document;
const WIN = window;
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
? DOC.currentScript : DOC.querySelector('script[src$="/a/w.js"]');
if (!SELF) return;
const site = String(SELF.getAttribute('data-site') || '');
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
const colorAttr = String(SELF.getAttribute('data-color') || '');
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
const base = src.replace(/[^/]*$/, '');
const endpoint = base + 'e';
// ---- Heatmap-Modus: nichts senden, kein Banner ----
const qs = new URLSearchParams(WIN.location.search);
const heatToken = qs.get('__wk_heat');
if (heatToken) {
qs.delete('__wk_heat');
const rest = qs.toString();
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
const s = DOC.createElement('script');
s.setAttribute('src', base + 'h.js');
s.setAttribute('data-token', heatToken);
(DOC.head || DOC.documentElement).appendChild(s);
return;
}
// ---- Senden: Fehler werden verschluckt ----
function send(obj) {
try {
const body = JSON.stringify(obj);
if (navigator.sendBeacon && navigator.sendBeacon(endpoint, body)) return;
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
} catch (e) { /* egal */ }
}
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
const vp = () => vpClass(WIN.innerWidth);
const ownHost = () => WIN.location.hostname;
// ---- Seitenaufruf (pv): immer, auch ohne Einwilligung ----
let lastPath = null;
let pvStart = Date.now();
let maxScroll = 0;
let enSent = false;
function sendPv(first) {
lastPath = cleanPath(WIN.location.pathname);
pvStart = Date.now(); maxScroll = 0; enSent = false;
send({ t: 'pv', site, path: lastPath, ref: first ? refHost(DOC.referrer, ownHost()) : '', vp: vp() });
}
let navTimer = null;
function scheduleNav() {
if (navTimer) return;
navTimer = setTimeout(safe(function () {
navTimer = null;
if (cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
}), 300);
}
sendPv(true);
try {
const orig = WIN.history.pushState;
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
WIN.addEventListener('popstate', safe(scheduleNav));
} catch (e) { /* egal */ }
// ---- Tracking (nur nach Annehmen erreichbar) ----
let tracking = false;
const hosts = [];
const scrollPct = () => {
const h = DOC.documentElement.scrollHeight || 1;
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
};
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
const onClick = safe(function (e) {
const t = e.target;
if (!t || hosts.indexOf(t) !== -1) return;
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
const path = cleanPath(WIN.location.pathname);
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
let a = t;
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
const kind = a ? contactKind(a, ownHost()) : null;
if (kind) {
const ev = { t: 'ct', site, path, kind };
if (kind === 'out') ev.host = linkHost(a);
send(ev);
}
});
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
const onLeave = safe(function () {
if (enSent) return;
enSent = true;
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
});
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
function startTracking() {
if (tracking) return;
tracking = true;
DOC.addEventListener('click', onClick, { capture: true, passive: true });
DOC.addEventListener('visibilitychange', onVis);
WIN.addEventListener('pagehide', onLeave);
WIN.addEventListener('scroll', onScroll, { passive: true });
}
function stopTracking() {
tracking = false;
DOC.removeEventListener('click', onClick, { capture: true });
DOC.removeEventListener('visibilitychange', onVis);
WIN.removeEventListener('pagehide', onLeave);
WIN.removeEventListener('scroll', onScroll);
}
// ---- Einwilligung ----
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1';
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
let storage = null;
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
const stored = readConsent(storage, Date.now());
let bannerHost = null;
let linkHostEl = null;
function choose(a) {
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ }
send({ t: 'bn', site, a });
if (a === 'accept') startTracking(); else stopTracking();
closeBanner();
}
function el(tag, attrs, text) {
const n = DOC.createElement(tag);
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
if (text != null) n.textContent = text;
return n;
}
const CSS = ':host{all:initial}' +
'*{box-sizing:border-box}' +
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
'.t{margin:0;max-width:720px}' +
'.t a{color:#111;text-decoration:underline}' +
'.r{display:flex;gap:10px;flex:none}' +
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' +
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
function openBanner() {
if (bannerHost) return;
const prev = DOC.activeElement;
bannerHost = DOC.createElement('div');
const st = bannerHost.style;
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
hosts.push(bannerHost);
const root = bannerHost.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, CSS));
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der ');
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info'));
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
const row = el('div', { class: 'r' });
const no = el('button', { type: 'button' }, 'Ablehnen');
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen');
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
row.appendChild(no); row.appendChild(yes);
bar.appendChild(p); bar.appendChild(row);
root.appendChild(bar);
DOC.body.appendChild(bannerHost);
if (linkHostEl) linkHostEl.style.display = 'none';
no.focus();
}
function closeBanner() {
if (!bannerHost) return;
const i = hosts.indexOf(bannerHost);
if (i !== -1) hosts.splice(i, 1);
bannerHost.remove();
bannerHost = null;
if (linkHostEl) linkHostEl.style.display = '';
}
function addSettingsLink() {
linkHostEl = DOC.createElement('div');
const st = linkHostEl.style;
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
hosts.push(linkHostEl);
const root = linkHostEl.attachShadow({ mode: 'open' });
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
b.addEventListener('click', safe(openBanner));
root.appendChild(b);
DOC.body.appendChild(linkHostEl);
}
if (DOC.querySelector('[data-webklar-consent]')) {
DOC.addEventListener('click', safe(function (e) {
const t = e.target;
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(); }
}));
} else {
addSettingsLink();
}
if (stored) {
if (stored.a === 'accept') startTracking();
} else {
openBanner();
send({ t: 'bn', site, a: 'shown' });
}
}
try { main(); } catch (e) { /* nie die Seite stoeren */ }