Summary
Build a self-hosted, adblocker-resistant analytics system with two parts:
- Tracking module in
ctw-kit - A lightweight component that auto-tracks pageviews and clicks, importable into any project.
- 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)
Phase 2: Backend + API (comms)
Phase 3: Dashboard (comms)
Phase 4: Redirect service
Phase 5: Integration
Related
Summary
Build a self-hosted, adblocker-resistant analytics system with two parts:
ctw-kit- A lightweight component that auto-tracks pageviews and clicks, importable into any project.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
Architecture
Tracking module (
ctw-kit)src/lib/components/Ink/Ink.svelte- drop into any layout to auto-trackink.ts- detects SSR vs client, sends eventsUsage
Tracking strategies by environment
hooks.server.tsfetchPOST to own domaingo.ctw.studio/slug)What to track (v1)
Adblocker evasion strategy
Adblockers match on domain names, script filenames, request URLs, and known third-party domains. To stay unblocked:
ctw-kit(already generic, no analytics-related words)Ink(neutral, not suspicious)/api/v1/s(looks like a normal API route, avoids/track,/collect,/beacon,/pixel,/analytics)fetchPOST (notnavigator.sendBeacon()which some blockers target)Comms dashboard (separate repo)
Dashboard features
API endpoint
POST /api/v1/s- Receives events from the Ink component, stores in PocketBaseRedirect service (for external link tracking)
go.ctw.studio/my-medium-postthat redirect to the destination after counting the clickredirects(slug, destination URL, click count)PocketBase schema
eventscollection:pagevieworclickredirectscollection:What's NOT possible (limitations)
go.ctw.studio/my-postinstead of the direct Medium URL) or by polling Medium's own stats.Implementation plan
Phase 1: Core tracking (ctw-kit)
src/lib/components/Ink/module in ctw-kitInk.sveltecomponent (auto pageview + click tracking)ink.tscore logic (SSR detection, event sending)ctw-kitindexPhase 2: Backend + API (comms)
/api/v1/sendpoint in comms SvelteKit app to receive eventsPhase 3: Dashboard (comms)
Phase 4: Redirect service
/r/:slugor similar)Phase 5: Integration
<Ink siteId="portfolio" />to portfolioRelated
ctw-kit/directory in this repo