Examples

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.

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

MethodDescriptionReturns
Track initial page viewPageViewResponse
Update engagement metrics{ success: boolean }
Get analytics statisticsAnalyticsStats

Track when a user views an article.

track-pageview.ts
import { 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'

PropertyTypeDescriptionDefault
slug*stringArticle slug to track-
sessionIdstringUnique session identifier-
pageUrlstringFull page URL-
pageTitlestringPage title-
referrerstringReferrer URL-
utmSourcestringUTM source parameter-
utmMediumstringUTM medium parameter-
utmCampaignstringUTM campaign parameter-
utmTermstringUTM term parameter-
utmContentstringUTM content parameter-
screenWidthnumberBrowser viewport width-
screenHeightnumberBrowser viewport height-
timeOnPagenumberTime spent on page (seconds)-
scrollDepthnumberMax scroll depth (0-100)-
exitPositionnumberScroll position when leaving-
bouncedbooleanWhether user bounced-
timestampstringCustom timestamp (ISO 8601)-
eventsPageEvent[]Custom events-
fetchGeobooleanFetch 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
})

PropertyTypeDescriptionDefault
timeOnPagenumberTotal time on page (seconds)-
scrollDepthnumberMaximum scroll depth reached (0-100)-
exitPositionnumberScroll position when exiting (0-100)-
bouncedbooleanWhether user bounced (left immediately)-
eventsPageEvent[]Custom events to add-

Track user interactions like clicks, video plays, or form submissions.

custom-events.ts
const 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.ts
interface 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 sessionStorage for 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 beforeunload event
  • Update on tab switch using visibilitychange event

  • 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

Quick Help

Track page views for content pages (articles, blog posts). Skip tracking for admin pages, API routes, or frequently visited pages like home. Focus on measuring content performance.
Use useRef to track if analytics was already sent. In development, React Strict Mode mounts components twice, so add a ref check to prevent double tracking.
No! Always track analytics asynchronously. Don't await the track() call in your component render. Use fire-and-forget pattern or background requests.

Command Palette

Search for a command to run...