Skip to content

SSG Analytics — Adding Analytics to Static Sites Without a Backend

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about SSG Analytics. We cover key concepts, practical examples, and best practices to help you master this topic.

Static site analytics uses client-side tracking with privacy-focused tools like Plausible, Fathom, or Google Analytics to measure traffic without a server.

What You'll Learn

By the end of this tutorial, you'll understand how to add analytics to SSG sites, the difference between traditional and privacy-focused analytics, how to track custom events, and how to measure Core Web Vitals.

Why It Matters

Without analytics, you can't measure what's working. SSG sites lack server logs, so client-side tracking is essential. Choosing the right analytics tool affects privacy, performance, and data accuracy.

Real-World Use

A documentation site uses Plausible Analytics for privacy-compliant traffic measurement. The script is under 1KB, doesn't use cookies, and provides pageviews, referrers, and custom events without affecting Lighthouse scores.

Analytics Architecture

graph TD
    A[User visits
static page] --> B[Script loads
from CDN] B --> C[Collect page data
URL, referrer, device] C --> D{Send to
analytics endpoint} D --> E[Plausible / Fathom
Cloud-hosted] D --> F[Google Analytics 4
Google Cloud] D --> G[Self-hosted
Your server] E --> H[Dashboard
Real-time traffic] F --> H G --> H style B fill:#4a90d9,color:#fff style C fill:#e67e22,color:#fff style H fill:#27ae60,color:#fff

Plausible Analytics Setup

<!-- Plausible — Privacy-first analytics -->
<!-- Add to <head> in base template -->
<script
    defer
    data-domain="example.com"
    src="https://plausible.io/js/script.js">
</script>

<!-- Custom event tracking -->
<script>
    // Track button clicks
    document.querySelectorAll('.download-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            plausible('Download', {
                props: {
                    file: btn.dataset.file,
                    format: btn.dataset.format
                }
            });
        });
    });

    // Track search
    document.getElementById('search-form')?.addEventListener('submit', (e) => {
        const query = e.target.querySelector('input').value;
        plausible('Search', { props: { query } });
    });
</script>

Fathom Analytics

<!-- Fathom — Lightweight analytics -->
<script src="https://cdn.usefathom.com/script.js"
        data-site="YOUR_SITE_ID"
        data-spa="auto"
        data-auto="false"
        defer>
</script>

<!-- Custom goals -->
<script>
    window.addEventListener('load', () => {
        // Track outbound links
        document.querySelectorAll('a[href^="http"]').forEach(link => {
            link.addEventListener('click', () => {
                fathom.trackGoal('OUTBOUND_CLICK', {
                    url: link.href,
                    text: link.textContent
                });
            });
        });

        // Track scroll depth
        let trackedDepths = new Set();
        window.addEventListener('scroll', () => {
            const depth = Math.round(
                (window.scrollY + window.innerHeight) /
                document.documentElement.scrollHeight * 100
            );
            [25, 50, 75, 100].forEach(threshold => {
                if (depth >= threshold && !trackedDepths.has(threshold)) {
                    trackedDepths.add(threshold);
                    fathom.trackGoal(`SCROLL_${threshold}`);
                }
            });
        });
    });
</script>

Google Analytics 4 (GA4)

<!-- Google Analytics 4 — gtag.js -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());

    // Configure with consent mode
    gtag('consent', 'default', {
        analytics_storage: 'denied',
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
        wait_for_update: 500
    });

    gtag('config', 'G-XXXXXXXXXX', {
        anonymize_ip: true,
        allow_google_signals: false,
        allow_ad_personalization_signals: false
    });

    // Track custom events
    function trackTutorialProgress(tutorialId, step) {
        gtag('event', 'tutorial_progress', {
            tutorial_id: tutorialId,
            step_number: step
        });
    }

    // Track downloads
    function trackDownload(fileName, fileType) {
        gtag('event', 'download', {
            file_name: fileName,
            file_type: fileType
        });
    }
</script>

Core Web Vitals Tracking

// web-vitals.js — Measure Core Web Vitals
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
    const body = {
        name: metric.name,
        value: metric.value,
        rating: metric.rating,
        delta: metric.delta,
        url: window.location.pathname,
        device: navigator.userAgent.match(/Mobile|Android/) ? 'mobile' : 'desktop',
        connection: navigator.connection?.effectiveType || 'unknown',
    };

    // Send to analytics endpoint
    if (typeof plausible !== 'undefined') {
        plausible('WebVital', {
            props: body
        });
    }

    if (typeof gtag !== 'undefined') {
        gtag('event', metric.name, {
            value: metric.value,
            metric_rating: metric.rating,
            metric_delta: metric.delta,
            event_category: 'Web Vitals',
            non_interaction: true
        });
    }

    // Also send to your own endpoint for custom monitoring
    fetch('/api/vitals', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        keepalive: true
    }).catch(() => {});
}

// Track all Core Web Vitals
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

Custom Analytics Dashboard

// public/analytics/dashboard.html — Simple analytics dashboard
// Uses localStorage for storage (or connect to a backend)
const Dashboard = {
    events: JSON.parse(localStorage.getItem('analytics_events') || '[]'),

    track(event, data = {}) {
        this.events.push({
            event,
            data,
            url: window.location.pathname,
            timestamp: new Date().toISOString(),
            referrer: document.referrer,
            userAgent: navigator.userAgent,
        });
        localStorage.setItem('analytics_events', JSON.stringify(this.events));
    },

    getPageviews() {
        const counts = {};
        this.events
            .filter(e => e.event === 'pageview')
            .forEach(e => {
                counts[e.url] = (counts[e.url] || 0) + 1;
            });
        return counts;
    },

    render() {
        const pageviews = this.getPageviews();
        const sorted = Object.entries(pageviews)
            .sort(([, a], [, b]) => b - a);

        console.log('=== Page Views ===');
        sorted.forEach(([url, count]) => {
            console.log(`${url}: ${count} views`);
        });
    }
};

// Track pageviews
Dashboard.track('pageview');

Common Mistakes

  1. Blocking page load with analytics scripts. Analytics scripts should use defer or async attribute. Never block content rendering for analytics.
  2. Tracking without user consent. GDPR, CCPA, and other privacy laws require consent before tracking. Use a consent management platform.
  3. Counting bot traffic as real users. Search engine crawlers and uptime monitors inflate numbers. Filter known bots using User-Agent detection.
  4. Not filtering your own traffic. Your visits to test the site pollute analytics. Set up IP filtering or a development environment exclusion.
  5. Over-relying on pageview count. Pageviews don't measure engagement. Track scroll depth, time on page, and conversion events for meaningful metrics.

Practice Questions

  1. Why do static sites need client-side analytics instead of server-side?
  2. How does Plausible differ from Google Analytics in terms of privacy?
  3. What are Core Web Vitals and how do you track them?
  4. How do you track custom events like button clicks or form submissions?
  5. What privacy considerations apply when adding analytics to a static site?

Challenge: Set up analytics for an SSG site: integrate Plausible or Fathom with a privacy-friendly configuration, track 3 custom events (downloads, search, scroll depth), implement Core Web Vitals tracking, and create a simple analytics dashboard.

FAQ

Do I need analytics for a small static site?

Yes. Even small sites benefit from understanding visitor behavior. Start with a free tool like Plausible or the free tier of Google Analytics.

How does analytics affect page performance?

Poorly implemented analytics can add 100ms+ to load time. Use lightweight scripts (Plausible: ~1KB, Fathom: ~2KB) loaded with defer attribute.

Can I self-host analytics for my SSG site?

Yes. Plausible and Fathom offer self-hosted versions. You need a server to receive analytics events. Self-hosting gives full data control.

How do I exclude my own visits from analytics?

IP filtering or cookie-based exclusion. Most analytics tools support filtering by IP range or setting a 'do not track' cookie on your browser.

What is the difference between pageviews and unique visitors?

Pageviews count every page load. Unique visitors count distinct users (identified by cookies or fingerprinting). Both metrics are useful for different purposes.

Mini Project

Integrate analytics into an SSG site: add Plausible Analytics with privacy-friendly configuration, track 5 custom events (page view, scroll depth, outbound link click, search, download), implement Core Web Vitals monitoring, and set up IP filtering to exclude internal traffic.

What's Next

Analytics is set up. Now build your final project: complete the SSG Mini Project to apply everything you've learned about static site generation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro