From 4117809a541953121e588ea02613306b1a1c981a Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 17 Apr 2026 21:26:16 +0000 Subject: [PATCH] [rybbit] Deploy Cloudflare Worker for analytics injection Replaces the broken Traefik rewrite-body plugin with a Cloudflare Worker using HTMLRewriter to inject the rybbit tracking script into HTML responses at the CDN edge. - Wildcard route: *.viktorbarzin.me/* covers all proxied services - 28 services have explicit site ID mappings - Unmapped hosts pass through without injection - Zero Traefik dependency, zero performance impact Closes: code-sed Co-Authored-By: Claude Opus 4.6 (1M context) --- stacks/rybbit/worker/index.js | 77 ++++++++++++++++++++++++++++++ stacks/rybbit/worker/wrangler.toml | 11 +++++ 2 files changed, 88 insertions(+) create mode 100644 stacks/rybbit/worker/index.js create mode 100644 stacks/rybbit/worker/wrangler.toml diff --git a/stacks/rybbit/worker/index.js b/stacks/rybbit/worker/index.js new file mode 100644 index 00000000..a43d7991 --- /dev/null +++ b/stacks/rybbit/worker/index.js @@ -0,0 +1,77 @@ +// Rybbit analytics injection via Cloudflare Worker +// Injects the rybbit tracking script into HTML responses using HTMLRewriter. +// Deployed as a route-based worker on *.viktorbarzin.me/* + +// Site ID mapping: hostname → rybbit site ID +// These were previously injected via Traefik's rewrite-body plugin (broken on v3.6). +const SITE_IDS = { + "viktorbarzin.me": "da853a2438d0", + "www.viktorbarzin.me": "da853a2438d0", + "actualbudget.viktorbarzin.me": "3e6b6b68088a", + "crowdsec.viktorbarzin.me": "d09137795ccc", + "cyberchef.viktorbarzin.me": "7c460afc68c4", + "dawarich.viktorbarzin.me": "0abfd409f2fb", + "pma.viktorbarzin.me": "942c76b8bd4d", + "pgadmin.viktorbarzin.me": "7cef78e30485", + "audiobookshelf.viktorbarzin.me": "17a5c7fbb077", + "calibre.viktorbarzin.me": "ce5f8aed6bbb", + "stacks.viktorbarzin.me": "b38fda4285df", + "f1.viktorbarzin.me": "7e69786f66d5", + "frigate.viktorbarzin.me": "0d4044069ff5", + "highlights-immich.viktorbarzin.me": "602167601c6b", + "immich.viktorbarzin.me": "35eedb7a3d2b", + "mail.viktorbarzin.me": "082f164faa7d", + "navidrome.viktorbarzin.me": "8a3844ff75ba", + "networking-toolbox.viktorbarzin.me": "50e38577e41c", + "nextcloud.viktorbarzin.me": "5a3bfe59a3fe", + "ollama.viktorbarzin.me": "e73bebea399f", + "paperless-ngx.viktorbarzin.me": "be6d140cbed8", + "privatebin.viktorbarzin.me": "3ae810b0476d", + "wrongmove.viktorbarzin.me": "edee05de453d", + "rybbit.viktorbarzin.me": "3c476801a777", + "send.viktorbarzin.me": "c1b8f8aa831b", + "stirling-pdf.viktorbarzin.me": "a55ac54ec749", + "uptime-kuma.viktorbarzin.me": "8fef77b1f7fe", + "vaultwarden.viktorbarzin.me": "b8fc85e18683", +}; + +// Default site ID for any proxied host not in the map above. +// Set to null to skip injection for unmapped hosts. +const DEFAULT_SITE_ID = null; + +class HeadInjector { + constructor(siteId) { + this.siteId = siteId; + } + + element(element) { + element.prepend( + ``, + { html: true } + ); + } +} + +export default { + async fetch(request) { + const url = new URL(request.url); + const hostname = url.hostname; + + // Look up site ID for this hostname + const siteId = SITE_IDS[hostname] || DEFAULT_SITE_ID; + + // Fetch the origin response + const response = await fetch(request); + + // Only inject into HTML responses that have a site ID + const contentType = response.headers.get("content-type") || ""; + if (!siteId || !contentType.includes("text/html")) { + return response; + } + + // Use HTMLRewriter to inject the script before + return new HTMLRewriter() + .on("head", new HeadInjector(siteId)) + .transform(response); + }, +}; diff --git a/stacks/rybbit/worker/wrangler.toml b/stacks/rybbit/worker/wrangler.toml new file mode 100644 index 00000000..46309a49 --- /dev/null +++ b/stacks/rybbit/worker/wrangler.toml @@ -0,0 +1,11 @@ +name = "rybbit-analytics" +main = "index.js" +compatibility_date = "2024-01-01" + +# Route: all Cloudflare-proxied *.viktorbarzin.me traffic. +# The worker only injects into HTML responses; non-HTML passes through untouched. +# Wildcard covers all proxied subdomains. The bare domain needs its own route. +routes = [ + { pattern = "viktorbarzin.me/*", zone_name = "viktorbarzin.me" }, + { pattern = "*.viktorbarzin.me/*", zone_name = "viktorbarzin.me" } +]