Simplist provides server-side analytics that are privacy-friendly and adblocker-proof. Track page views, engagement metrics, and get detailed insights about your content performance.
For the simplest setup, use our drop-in analytics script. One line of HTML, no SDK required:
index.html<script defer src="https://cdn.simplist.blog/analytics.js" data-project="your-project-slug"></script>This automatically tracks:
- Page views and unique visitors
- Scroll depth milestones (25%, 50%, 75%, 90%)
- Time on page
- Referrers and UTM parameters
- Geo location
The script auto-detects article slugs from your URL and handles all engagement tracking. For full documentation, see the Analytics Script Guide.
Use the script for quick, zero-maintenance tracking. Use the SDK when you need custom events, server-side tracking, or programmatic control.
Traditional client-side analytics (like Google Analytics) face several challenges:
- Blocked by ad blockers - Users with privacy tools never get tracked
- Privacy concerns - JavaScript trackers collect extensive user data
- GDPR compliance - Requires cookie banners and consent
- Inaccurate data - Bot traffic and script blockers skew results
Simplist server-side analytics solves these problems:
- Adblocker-proof - Runs on your server, not in the browser
- Privacy-focused - No cookies, no personal data collection
- Accurate tracking - Real page views from real users
- Simple integration - Just one API call per page view
| Method | Description | Returns |
|---|---|---|
| Track initial page view | PageViewResponse | |
| Update engagement metrics | { success: boolean } | |
| Get analytics statistics | AnalyticsStats |
Track when a user views an article.
track-pageview.tsimport { SimplistClient } from "@simplist.blog/sdk"
// Uses SIMPLIST_API_KEY from environment variables
const client = new SimplistClient()
const result = await client.analytics.track({
slug: 'my-article-slug',
sessionId: 'session_abc123',
referrer: 'https://google.com',
pageUrl: 'https://myblog.com/articles/my-article',
pageTitle: 'My Article Title',
screenWidth: 1920,
screenHeight: 1080,
fetchGeo: true
})
console.log(result.pageViewId) // 'pv_xyz789'
console.log(result.visitorId) // 'visitor_def456'
console.log(result.sessionId) // 'session_abc123'| Property | Type | Description | Default |
|---|---|---|---|
slug* | string | Article slug to track | - |
sessionId | string | Unique session identifier | - |
pageUrl | string | Full page URL | - |
pageTitle | string | Page title | - |
referrer | string | Referrer URL | - |
utmSource | string | UTM source parameter | - |
utmMedium | string | UTM medium parameter | - |
utmCampaign | string | UTM campaign parameter | - |
utmTerm | string | UTM term parameter | - |
utmContent | string | UTM content parameter | - |
screenWidth | number | Browser viewport width | - |
screenHeight | number | Browser viewport height | - |
timeOnPage | number | Time spent on page (seconds) | - |
scrollDepth | number | Max scroll depth (0-100) | - |
exitPosition | number | Scroll position when leaving | - |
bounced | boolean | Whether user bounced | - |
timestamp | string | Custom timestamp (ISO 8601) | - |
events | PageEvent[] | Custom events | - |
fetchGeo | boolean | Fetch geolocation data | - |
The track() method returns a PageViewResponse:
response.ts{
success: true,
pageViewId: 'pv_xyz789', // Use this to update metrics later
visitorId: 'visitor_def456', // Unique visitor ID
sessionId: 'session_abc123' // Session ID
}After tracking the initial page view, update it with engagement metrics when the user leaves.
update-metrics.ts// Track initial page view
const result = await client.analytics.track({
slug: 'my-article-slug',
sessionId: 'session_abc123'
})
// Later, when user leaves the page
await client.analytics.update(result.pageViewId, {
timeOnPage: 245, // User spent 245 seconds
scrollDepth: 85, // Scrolled 85% of the page
exitPosition: 60, // Left at 60% scroll position
bounced: false // Did not bounce
})| Property | Type | Description | Default |
|---|---|---|---|
timeOnPage | number | Total time on page (seconds) | - |
scrollDepth | number | Maximum scroll depth reached (0-100) | - |
exitPosition | number | Scroll position when exiting (0-100) | - |
bounced | boolean | Whether user bounced (left immediately) | - |
events | PageEvent[] | Custom events to add | - |
Track user interactions like clicks, video plays, or form submissions.
custom-events.tsconst result = await client.analytics.track({
slug: 'my-article-slug',
sessionId: 'session_abc123',
events: [
{
type: 'button_click',
element: '#cta-button',
data: { label: 'Sign up now' },
timestamp: new Date().toISOString()
},
{
type: 'video_play',
element: '#intro-video',
position: 40, // 40% down the page
data: { duration: 120 }
}
]
})
// Or update with events later
await client.analytics.update(result.pageViewId, {
events: [
{
type: 'link_click',
element: 'a.external',
data: { href: 'https://example.com' }
}
]
})page-event.tsinterface PageEvent {
type: string // Event type (e.g., 'click', 'scroll')
data?: Record<string, any> // Custom event data
position?: number // Position on page (0-100)
element?: string // CSS selector or element ID
timestamp?: string // ISO 8601 timestamp
timeOffset?: number // Offset from page load (seconds)
}Retrieve analytics data for your project.
get-stats.ts// Get last 30 days (default)
const stats = await client.analytics.getStats()
// Get last 7 days
const weekStats = await client.analytics.getStats({ days: 7 })
// Get last 90 days
const quarterStats = await client.analytics.getStats({ days: 90 })analytics-stats.ts{
period: {
days: 30,
startDate: '2025-11-08T00:00:00Z',
endDate: '2025-12-08T23:59:59Z'
},
summary: {
totalViews: 15234,
uniqueVisitors: 8921,
avgViewsPerVisitor: 1.71
},
requestSource: {
sdk: {
count: 12000,
percentage: 78.8
},
direct: {
count: 3234,
percentage: 21.2
}
},
topArticles: [
{
articleId: 'art_123',
title: 'Getting Started with React',
slug: 'getting-started-react',
views: 1234
},
// ... more articles
],
topCountries: [
{ country: 'US', views: 4532 },
{ country: 'GB', views: 2341 },
{ country: 'CA', views: 1876 }
]
}Here's a full implementation for a Next.js blog:
"use client"
import { useEffect, useRef } from "react";
import { SimplistClient } from "@simplist.blog/sdk";
import { getOrCreateSessionId } from "../lib/session";
// Uses SIMPLIST_API_KEY from environment variables
const client = new SimplistClient()
export function ArticleAnalytics({ slug }: { slug: string }) {
const pageViewIdRef = useRef<string | null>(null)
const startTimeRef = useRef<number>(Date.now())
const maxScrollRef = useRef<number>(0)
useEffect(() => {
const sessionId = getOrCreateSessionId()
const trackPageView = async () => {
try {
const result = await client.analytics.track({
slug,
sessionId,
pageUrl: window.location.href,
pageTitle: document.title,
referrer: document.referrer,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
fetchGeo: true
})
pageViewIdRef.current = result.pageViewId
} catch (error) {
console.error('Analytics tracking error:', error)
}
}
trackPageView()
const handleScroll = () => {
const scrolled =
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
maxScrollRef.current = Math.max(maxScrollRef.current, scrolled)
}
window.addEventListener('scroll', handleScroll, { passive: true })
const updateMetrics = async () => {
if (!pageViewIdRef.current) return
const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)
const scrollDepth = Math.round(maxScrollRef.current)
const bounced = timeOnPage < 5 && scrollDepth < 25
try {
await client.analytics.update(pageViewIdRef.current, {
timeOnPage,
scrollDepth,
exitPosition: Math.round(
(window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
),
bounced
})
} catch (error) {
console.error('Analytics update error:', error)
}
}
window.addEventListener('beforeunload', updateMetrics)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
updateMetrics()
}
})
return () => {
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('beforeunload', updateMetrics)
}
}, [slug])
return null
}Generate a unique session ID and persist it:
- Use
sessionStoragefor same-tab continuity - Expire sessions after 30 minutes of inactivity
- Include session ID in all tracking calls
- Track immediately on page load
- Update on exit using
beforeunloadevent - Update on tab switch using
visibilitychangeevent
- No personal data - Don't send names, emails, or IDs
- No cookies - Use session storage instead
- Opt-out respect - Check Do Not Track settings
- GDPR compliant - No consent banner needed
- Track analytics after content loads
- Use passive event listeners for scroll tracking
- Debounce scroll events to reduce CPU usage
- Send updates asynchronously
Automatically track marketing campaigns with UTM parameters:
utm-tracking.ts// Parse URL parameters
const params = new URLSearchParams(window.location.search)
await client.analytics.track({
slug: 'my-article',
sessionId: getSessionId(),
utmSource: params.get('utm_source') ?? undefined,
utmMedium: params.get('utm_medium') ?? undefined,
utmCampaign: params.get('utm_campaign') ?? undefined,
utmTerm: params.get('utm_term') ?? undefined,
utmContent: params.get('utm_content') ?? undefined
})
// Example URL:
// https://myblog.com/article?utm_source=twitter&utm_medium=social&utm_campaign=launch- Articles API - Fetch and manage articles
- SEO Utilities - Generate sitemaps and structured data
- Complete Examples - Full integration examples