SSG Analytics — Adding Analytics to Static Sites Without a Backend
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
- Blocking page load with analytics scripts. Analytics scripts should use defer or async attribute. Never block content rendering for analytics.
- Tracking without user consent. GDPR, CCPA, and other privacy laws require consent before tracking. Use a consent management platform.
- Counting bot traffic as real users. Search engine crawlers and uptime monitors inflate numbers. Filter known bots using User-Agent detection.
- Not filtering your own traffic. Your visits to test the site pollute analytics. Set up IP filtering or a development environment exclusion.
- Over-relying on pageview count. Pageviews don't measure engagement. Track scroll depth, time on page, and conversion events for meaningful metrics.
Practice Questions
- Why do static sites need client-side analytics instead of server-side?
- How does Plausible differ from Google Analytics in terms of privacy?
- What are Core Web Vitals and how do you track them?
- How do you track custom events like button clicks or form submissions?
- 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
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