Skip to content

Add self-hosted analytics tracking to ctw-kit + comms dashboard #141

Description

@ctwhome

Summary

Build a self-hosted, adblocker-resistant analytics system with two parts:

  1. Tracking module in ctw-kit - A lightweight component that auto-tracks pageviews and clicks, importable into any project.
  2. Comms dashboard (separate repo: ctwhome/comms) - A SvelteKit app that receives events, stores them in PocketBase, and displays analytics (views, clicks, impact, telemetry).

The goal is to replace Google Analytics with a custom solution that adblockers won't block, and to have a centralized dashboard to measure impact across all projects.


Motivation

  • Google Analytics gets blocked by most adblockers, resulting in inaccurate data.
  • A self-hosted solution on your own domain with generic naming won't appear in any adblocker filter lists.
  • Centralized analytics across all projects (websites, blog posts, social media links) in one dashboard.

Architecture

Your sites → import { Ink } from 'ctw-kit' → POST to comms.ctw.studio/api/v1/s → PocketBase
                                                          ↓
                                                comms dashboard reads + displays

Tracking module (ctw-kit)

  • Location: src/lib/components/Ink/
  • Component: Ink.svelte - drop into any layout to auto-track
  • Core logic: ink.ts - detects SSR vs client, sends events

Usage

<!-- +layout.svelte -->
<script>
  import { Ink } from 'ctw-kit';
</script>
<Ink siteId="my-project" />

Tracking strategies by environment

Environment Strategy Blockable?
SvelteKit on Vercel (SSR) Send pageview server-side from hooks.server.ts No - server-side, unblockable
Static sites (GitHub Pages) Client-side fetch POST to own domain No - own domain, generic path, not in filter lists
External content (Medium, LinkedIn) Redirect links through own domain (e.g., go.ctw.studio/slug) No - server-side redirect

What to track (v1)

  • Pageviews: URL, referrer, timestamp, user agent, screen size, site ID
  • Clicks: element tag, id, href, text content, position on page

Adblocker evasion strategy

Adblockers match on domain names, script filenames, request URLs, and known third-party domains. To stay unblocked:

  • Package name: ctw-kit (already generic, no analytics-related words)
  • Module name: Ink (neutral, not suspicious)
  • Endpoint path: /api/v1/s (looks like a normal API route, avoids /track, /collect, /beacon, /pixel, /analytics)
  • Script internals: No filenames or variable names containing analytics/tracking/telemetry keywords
  • Request method: Standard fetch POST (not navigator.sendBeacon() which some blockers target)
  • Same-origin requests: When possible, requests go to the same domain the script is served from
  • Self-hosted: All data goes to your own domain, which won't be in any adblocker filter list

Comms dashboard (separate repo)

  • Repo: ctwhome/comms (private, already created)
  • Stack: SvelteKit + TypeScript + TailwindCSS + DaisyUI + Bun
  • Deployed on: Vercel
  • Backend: PocketBase

Dashboard features

  • Overview: Total views, clicks, engagement rate across all projects
  • Per-project breakdown: Filter analytics by site ID
  • Views over time: Line/bar charts
  • Top pages: Most viewed URLs per project
  • Click heatmap data: Most clicked elements
  • Referrer analysis: Where traffic comes from

API endpoint

  • POST /api/v1/s - Receives events from the Ink component, stores in PocketBase

Redirect service (for external link tracking)

  • Short links like go.ctw.studio/my-medium-post that redirect to the destination after counting the click
  • Useful for tracking clicks on links shared on social media, newsletters, etc.
  • PocketBase collection: redirects (slug, destination URL, click count)

PocketBase schema

events collection:

Field Type Description
site_id text Project identifier
type text pageview or click
url text Page URL
referrer text Referrer URL
user_agent text Browser user agent
screen_width number Viewport width
screen_height number Viewport height
element_tag text Clicked element tag (clicks only)
element_id text Clicked element id (clicks only)
element_href text Clicked element href (clicks only)
element_text text Clicked element text (clicks only)
country text Derived from IP (server-side)
session_id text Anonymous session identifier
created datetime Auto-generated

redirects collection:

Field Type Description
slug text Short link slug (unique)
destination text Target URL
clicks number Total click count
created datetime Auto-generated

What's NOT possible (limitations)

  • Medium posts: Cannot inject custom scripts. Tracking is only possible via redirect links (share go.ctw.studio/my-post instead of the direct Medium URL) or by polling Medium's own stats.
  • LinkedIn posts: Same limitation. Use redirect links for trackable sharing.
  • These platforms don't allow custom JavaScript, so only outbound link tracking through redirects works.

Implementation plan

Phase 1: Core tracking (ctw-kit)

  • Create src/lib/components/Ink/ module in ctw-kit
  • Implement Ink.svelte component (auto pageview + click tracking)
  • Implement ink.ts core logic (SSR detection, event sending)
  • Export from ctw-kit index
  • Publish new version of ctw-kit to npm

Phase 2: Backend + API (comms)

  • Set up PocketBase instance with events and redirects collections
  • Create /api/v1/s endpoint in comms SvelteKit app to receive events
  • Add IP-to-country resolution server-side
  • Deploy comms to Vercel

Phase 3: Dashboard (comms)

  • Build analytics dashboard UI (charts, stats, filters)
  • Per-project filtering by site ID
  • Views over time chart
  • Top pages ranking
  • Referrer breakdown
  • Click analytics

Phase 4: Redirect service

  • Build redirect endpoint (/r/:slug or similar)
  • Admin UI to create/manage short links
  • Click counting per redirect

Phase 5: Integration

  • Add <Ink siteId="portfolio" /> to portfolio
  • Add to other projects
  • Test adblocker resistance across uBlock Origin, AdBlock Plus, Brave shields

Related

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions