You don't need a dedicated data team or a SaaS analytics subscription to know which of your marketing campaigns are driving real conversions. With Cloudflare Workers, a D1 SQLite database, and a few hundred lines of code, you can build an analytics dashboard that tracks page views, click-throughs, and signups — tied directly to the content and features that drive them. ## The Problem Marketing teams at salon software companies face a blind spot. Google Analytics tells you how many people visited a page. Your CRM tells you who signed up. But neither tells you which *specific comparison page*, which *blog post*, or which *feature highlight* actually moved someone from visitor to lead. AiSalonHub ran into this exact wall. They publish comparison pages — "Salon Software A vs. B" articles — that are among their highest-traffic content. But when a visitor landed on a comparison page, clicked through to a feature demo, and then signed up, the chain was invisible. Was it the pricing table? The feature matrix? A specific callout about automated scheduling? Without this data, the marketing team was guessing. They poured budget into content without knowing what converted. They A/B tested headlines but couldn't tell if the winning variant actually drove signups or just clicks. The core problem: **no real-time, content-level analytics pipeline tied directly to conversion events.** ## The Solution AiSalonHub built a lightweight event tracking system on top of Cloudflare Workers and D1. The architecture is simple: - **Cloudflare Workers** act as the event ingress point — capturing page views, link clicks, and signup completions from the frontend. - **D1** (Cloudflare's serverless SQLite database) stores every event in a structured schema. - **A simple dashboard Worker** queries D1 with aggregation SQL and returns JSON that a frontend renders as charts. No Kafka. No Redshift. No third-party analytics SDK. Just Workers + D1 running on Cloudflare's edge network, costing pennies per month. The tracking is triggered by lightweight JavaScript snippets embedded in the marketing site. Each event call is a `POST` to the ingress Worker with a minimal payload: ```json { "event": "page_view", "page": "/comparison/software-a-vs-software-b", "session_id": "abc123", "referrer": "google", "user_agent": "Mozilla/5.0...", "timestamp": "2026-05-11T20:30:00Z" } ``` The ingress Worker validates the payload, enriches it with geographic data from the request's `CF-IPCountry` header, and inserts it into D1. For click events, the payload includes a `target` field identifying which element was clicked — for example, `"target": "cta-feature-scheduling"` or `"target": "pricing-table-row-premium"`. ## Architecture — SQL Schema The analytics database at AiSalonHub uses four core tables: ### `events` table — the raw event log ```sql CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, -- 'page_view', 'click', 'signup' page TEXT NOT NULL, -- URL path session_id TEXT NOT NULL, target TEXT, -- element clicked (NULL for page views) referrer TEXT, country TEXT, feature_tag TEXT, -- e.g. 'scheduling', 'inventory' created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_events_page ON events(page); CREATE INDEX idx_events_type ON events(event_type); CREATE INDEX idx_events_created ON events(created_at); ``` ### `page_metrics` table — pre-aggregated daily rollups ```sql CREATE TABLE page_metrics ( page TEXT NOT NULL, date TEXT NOT NULL, views INTEGER DEFAULT 0, unique_sessions INTEGER DEFAULT 0, clicks INTEGER DEFAULT 0, signups INTEGER DEFAULT 0, conversion_rate REAL DEFAULT 0.0, PRIMARY KEY (page, date) ); ``` ### `feature_conversions` table — which features drive signups ```sql CREATE TABLE feature_conversions ( feature_tag TEXT NOT NULL, date TEXT NOT NULL, impressions INTEGER DEFAULT 0, -- times feature was visible/clicked signups INTEGER DEFAULT 0, -- signups that came through this feature PRIMARY KEY (feature_tag, date) ); ``` ### `campaign_sources` table — traffic attribution ```sql CREATE TABLE campaign_sources ( campaign TEXT NOT NULL, source TEXT NOT NULL, -- 'google', 'direct', 'twitter', etc. date TEXT NOT NULL, visitors INTEGER DEFAULT 0, conversions INTEGER DEFAULT 0, cost_cents INTEGER DEFAULT 0, -- ad spend in cents, if trackable PRIMARY KEY (campaign, source, date) ); ``` The beauty of this schema is its simplicity. No joins across five tables to answer "what's the conversion rate for comparison pages this week." The `page_metrics` table already has it aggregated by day, and the dashboard Worker just runs a `SELECT` with a `WHERE date >= ?` clause. ## Implementation ### Event Ingress Worker The ingress Worker is the entry point for all tracking data. It runs on every page load and every significant user interaction on the marketing site. ```javascript // Ingress Worker — receives events from the marketing frontend export default { async fetch(request, env) { if (request.method !== 'POST') { return new Response('Method not allowed', { status: 405 }); } const body = await request.json(); const { event_type, page, session_id, target, referrer, feature_tag } = body; // Validate required fields if (!event_type || !page || !session_id) { return new Response('Missing required fields', { status: 400 }); } // Insert raw event await env.DB.prepare( `INSERT INTO events (event_type, page, session_id, target, referrer, country, feature_tag) VALUES (?, ?, ?, ?, ?, ?, ?)` ).bind( event_type, page, session_id, target || null, referrer || null, request.cf?.country || null, feature_tag || null ).run(); // If it's a signup, also update the page_metrics rollup immediately if (event_type === 'signup') { const today = new Date().toISOString().split('T')[0]; await env.DB.prepare( `INSERT INTO page_metrics (page, date, signups) VALUES (?, ?, 1) ON CONFLICT(page, date) DO UPDATE SET signups = signups + 1` ).bind(page, today).run(); } return new Response('OK', { status: 200 }); } }; ``` ### Dashboard Aggregation Worker The dashboard Worker runs the queries that power the frontend charts. It accepts query parameters for date ranges and page filters. ```javascript // Dashboard Worker — serves aggregated data to the frontend export default { async fetch(request, env) { const url = new URL(request.url); const days = parseInt(url.searchParams.get('days') || '30'); const pageFilter = url.searchParams.get('page'); const dateCutoff = new Date(); dateCutoff.setDate(dateCutoff.getDate() - days); const cutoff = dateCutoff.toISOString().split('T')[0]; let pageClause = ''; let bindings = [cutoff]; if (pageFilter) { pageClause = 'AND page = ?'; bindings.push(pageFilter); } // Top pages by conversion rate const topPages = await env.DB.prepare(` SELECT page, SUM(views) as total_views, SUM(signups) as total_signups, ROUND(CAST(SUM(signups) AS REAL) / NULLIF(SUM(views), 0) * 100, 2) as conv_rate FROM page_metrics WHERE date >= ? ${pageClause} GROUP BY page ORDER BY conv_rate DESC LIMIT 20 `).bind(...bindings).all(); // Feature conversion breakdown const features = await env.DB.prepare(` SELECT feature_tag, SUM(impressions) as total_impressions, SUM(signups) as total_signups, ROUND(CAST(SUM(signups) AS REAL) / NULLIF(SUM(impressions), 0) * 100, 2) as conv_rate FROM feature_conversions WHERE date >= ? GROUP BY feature_tag ORDER BY conv_rate DESC `).bind(cutoff).all(); // Daily trend data const dailyTrend = await env.DB.prepare(` SELECT date, SUM(views) as views, SUM(signups) as signups FROM page_metrics WHERE date >= ? ${pageClause} GROUP BY date ORDER BY date ASC `).bind(...bindings).all(); return new Response(JSON.stringify({ topPages: topPages.results, features: features.results, dailyTrend: dailyTrend.results }), { headers: { 'Content-Type': 'application/json' } }); } }; ``` ### Frontend Embed Snippet The marketing site includes a tiny JavaScript snippet that fires events. It's designed to be non-blocking and resilient — failures to reach the analytics Worker don't affect page rendering. ```javascript // Embedded in every page of the marketing site (function() { const ANALYTICS_URL = 'https://analytics.aisalonhub.com/ingress'; const sessionId = localStorage.getItem('session_id') || crypto.randomUUID(); localStorage.setItem('session_id', sessionId); function track(eventType, extra = {}) { const payload = { event_type: eventType, page: window.location.pathname, session_id: sessionId, referrer: document.referrer || null, ...extra }; // Fire and forget — don't block page rendering navigator.sendBeacon(ANALYTICS_URL, JSON.stringify(payload)); } // Track page view on load track('page_view'); // Track clicks on tracked elements document.addEventListener('click', (e) => { const el = e.target.closest('[data-analytics]'); if (el) { track('click', { target: el.dataset.analytics, feature_tag: el.dataset.feature || null }); } }); })(); ``` The magic is in the `data-analytics` attribute. Any element on the page tagged with `data-analytics="cta-try-demo"` or `data-analytics="feature-card-inventory"` automatically gets tracked. No extra JavaScript wiring needed. ## Results — What the Data Revealed Within two weeks of deploying the dashboard, AiSalonHub uncovered patterns that reshaped their marketing strategy. ### Comparison Pages Drive 3x More Signups Than Blog Posts | Page Type | Avg. Views/Day | Avg. Signups/Day | Conv. Rate | |---|---|---|---| | Comparison pages | 1,240 | 18 | 1.45% | | Blog posts | 3,800 | 14 | 0.37% | | Feature pages | 890 | 11 | 1.24% | | Homepage | 2,100 | 9 | 0.43% | Comparison pages had 1.45% conversion rate — nearly **4x higher** than blog posts. This was counterintuitive because blog posts drove more total traffic. The dashboard made it obvious: invest more in comparison content, even if it has lower raw traffic.
Key Takeaways
- **Key point 1** — summarize the main finding
- **Key point 2** — best practice or lesson
- **Key point 3** — actionable recommendation