Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit bd1b3ad

Browse files
committed
feat(blog): add PostHog analytics events for blog pages
- Add analytics.ts with blog-specific tracking events: - trackBlogIndexView: Track blog index views with post count - trackBlogPostView: Track individual post views with metadata - trackBlogPostScrollDepth: Track reading progress (25%, 50%, 75%, 100%) - trackBlogPostTimeSpent: Track time spent on posts - trackBlogPostShare: Track social share clicks - trackBlogPostCTAClick: Track CTA engagement - Add BlogIndexAnalytics and BlogPostAnalytics client components - Integrate analytics into /blog and /blog/[slug] pages Attribution is handled by PostHog save_referrer and save_campaign_params. MKT-74
1 parent 6f78205 commit bd1b3ad

5 files changed

Lines changed: 221 additions & 0 deletions

File tree

apps/web-roo-code/src/app/blog/[slug]/page.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getArticleStructuredData,
1313
getBlogPostBreadcrumbStructuredData,
1414
} from "@/lib/blog"
15+
import { BlogPostAnalytics } from "@/components/blog/blog-analytics"
1516

1617
// Force dynamic rendering to evaluate publish gating at request-time
1718
export const dynamic = "force-dynamic"
@@ -87,6 +88,17 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
8788
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }} />
8889
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />
8990

91+
{/* PostHog Analytics */}
92+
<BlogPostAnalytics
93+
post={{
94+
slug: post.slug,
95+
title: post.title,
96+
description: post.description,
97+
tags: post.tags,
98+
publish_date: post.publish_date,
99+
}}
100+
/>
101+
90102
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
91103
<article className="mx-auto max-w-3xl">
92104
{/* Back link */}

apps/web-roo-code/src/app/blog/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getBlogCollectionStructuredData,
99
getBlogBreadcrumbStructuredData,
1010
} from "@/lib/blog"
11+
import { BlogIndexAnalytics } from "@/components/blog/blog-analytics"
1112

1213
// Force dynamic rendering to evaluate publish gating at request-time
1314
export const dynamic = "force-dynamic"
@@ -65,6 +66,9 @@ export default function BlogIndex() {
6566
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(collectionSchema) }} />
6667
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} />
6768

69+
{/* PostHog Analytics */}
70+
<BlogIndexAnalytics postCount={posts.length} />
71+
6872
<div className="container mx-auto px-4 py-12 sm:px-6 lg:px-8">
6973
<div className="mx-auto max-w-4xl">
7074
{/* Page Header */}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client"
2+
3+
import { useEffect, useRef } from "react"
4+
import type { BlogPost } from "@/lib/blog"
5+
import { trackBlogIndexView, trackBlogPostView, trackBlogPostScrollDepth, trackBlogPostTimeSpent } from "@/lib/blog"
6+
7+
interface BlogIndexAnalyticsProps {
8+
postCount: number
9+
}
10+
11+
/**
12+
* Client component that tracks blog index page view
13+
* Place this inside the blog index page
14+
*/
15+
export function BlogIndexAnalytics({ postCount }: BlogIndexAnalyticsProps) {
16+
const tracked = useRef(false)
17+
18+
useEffect(() => {
19+
if (!tracked.current) {
20+
trackBlogIndexView(postCount)
21+
tracked.current = true
22+
}
23+
}, [postCount])
24+
25+
return null
26+
}
27+
28+
interface BlogPostAnalyticsProps {
29+
post: {
30+
slug: string
31+
title: string
32+
description: string
33+
tags: string[]
34+
publish_date: string
35+
}
36+
}
37+
38+
/**
39+
* Client component that tracks blog post view, scroll depth, and time spent
40+
* Place this inside the blog post page
41+
*/
42+
export function BlogPostAnalytics({ post }: BlogPostAnalyticsProps) {
43+
const trackedView = useRef(false)
44+
const trackedDepths = useRef<Set<25 | 50 | 75 | 100>>(new Set())
45+
const startTime = useRef<number>(Date.now())
46+
47+
useEffect(() => {
48+
// Capture start time for cleanup function
49+
const effectStartTime = startTime.current
50+
51+
// Track page view on mount
52+
if (!trackedView.current) {
53+
trackBlogPostView(post as BlogPost)
54+
trackedView.current = true
55+
}
56+
57+
// Track scroll depth
58+
const handleScroll = () => {
59+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight
60+
if (scrollHeight <= 0) return
61+
62+
const scrollPercent = (window.scrollY / scrollHeight) * 100
63+
64+
const depths: (25 | 50 | 75 | 100)[] = [25, 50, 75, 100]
65+
for (const depth of depths) {
66+
if (scrollPercent >= depth && !trackedDepths.current.has(depth)) {
67+
trackedDepths.current.add(depth)
68+
trackBlogPostScrollDepth(post as BlogPost, depth)
69+
}
70+
}
71+
}
72+
73+
// Track time spent on page when leaving
74+
const handleVisibilityChange = () => {
75+
if (document.visibilityState === "hidden") {
76+
const timeSpent = Date.now() - effectStartTime
77+
trackBlogPostTimeSpent(post as BlogPost, timeSpent)
78+
}
79+
}
80+
81+
window.addEventListener("scroll", handleScroll, { passive: true })
82+
document.addEventListener("visibilitychange", handleVisibilityChange)
83+
84+
// Check initial scroll position
85+
handleScroll()
86+
87+
return () => {
88+
window.removeEventListener("scroll", handleScroll)
89+
document.removeEventListener("visibilitychange", handleVisibilityChange)
90+
91+
// Track time spent when component unmounts
92+
const timeSpent = Date.now() - effectStartTime
93+
trackBlogPostTimeSpent(post as BlogPost, timeSpent)
94+
}
95+
// eslint-disable-next-line react-hooks/exhaustive-deps
96+
}, [post.slug])
97+
98+
return null
99+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Blog Analytics Events
3+
*
4+
* PostHog tracking events for the blog section.
5+
* These events help understand blog engagement and attribution.
6+
*/
7+
8+
import posthog from "posthog-js"
9+
import type { BlogPost } from "./types"
10+
11+
/**
12+
* Track blog index page view
13+
* Called when user views /blog
14+
*/
15+
export function trackBlogIndexView(postCount: number): void {
16+
if (typeof window === "undefined" || !posthog.__loaded) return
17+
18+
posthog.capture("blog_index_viewed", {
19+
post_count: postCount,
20+
page_type: "blog_index",
21+
})
22+
}
23+
24+
/**
25+
* Track blog post view
26+
* Called when user views /blog/[slug]
27+
*/
28+
export function trackBlogPostView(post: BlogPost): void {
29+
if (typeof window === "undefined" || !posthog.__loaded) return
30+
31+
posthog.capture("blog_post_viewed", {
32+
post_slug: post.slug,
33+
post_title: post.title,
34+
post_tags: post.tags,
35+
publish_date: post.publish_date,
36+
page_type: "blog_post",
37+
})
38+
}
39+
40+
/**
41+
* Track blog post scroll depth
42+
* Called at various scroll thresholds (25%, 50%, 75%, 100%)
43+
*/
44+
export function trackBlogPostScrollDepth(post: BlogPost, depth: 25 | 50 | 75 | 100): void {
45+
if (typeof window === "undefined" || !posthog.__loaded) return
46+
47+
posthog.capture("blog_post_scroll_depth", {
48+
post_slug: post.slug,
49+
post_title: post.title,
50+
scroll_depth: depth,
51+
})
52+
}
53+
54+
/**
55+
* Track blog post share
56+
* Called when user clicks a share button
57+
*/
58+
export function trackBlogPostShare(post: BlogPost, platform: string): void {
59+
if (typeof window === "undefined" || !posthog.__loaded) return
60+
61+
posthog.capture("blog_post_shared", {
62+
post_slug: post.slug,
63+
post_title: post.title,
64+
share_platform: platform,
65+
})
66+
}
67+
68+
/**
69+
* Track blog post CTA click
70+
* Called when user clicks a CTA within a blog post
71+
*/
72+
export function trackBlogPostCTAClick(post: BlogPost, ctaType: string, ctaTarget: string): void {
73+
if (typeof window === "undefined" || !posthog.__loaded) return
74+
75+
posthog.capture("blog_post_cta_click", {
76+
post_slug: post.slug,
77+
post_title: post.title,
78+
cta_type: ctaType,
79+
cta_target: ctaTarget,
80+
})
81+
}
82+
83+
/**
84+
* Track time spent on blog post
85+
* Called when user leaves the page (or at intervals)
86+
*/
87+
export function trackBlogPostTimeSpent(post: BlogPost, timeMs: number): void {
88+
if (typeof window === "undefined" || !posthog.__loaded) return
89+
90+
posthog.capture("blog_post_time_spent", {
91+
post_slug: post.slug,
92+
post_title: post.title,
93+
time_spent_ms: timeMs,
94+
time_spent_seconds: Math.round(timeMs / 1000),
95+
})
96+
}

apps/web-roo-code/src/lib/blog/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ export {
4444
getBlogPostBreadcrumbStructuredData,
4545
getBlogPostUrl,
4646
} from "./structured-data"
47+
48+
// Analytics (PostHog events)
49+
export {
50+
trackBlogIndexView,
51+
trackBlogPostView,
52+
trackBlogPostScrollDepth,
53+
trackBlogPostShare,
54+
trackBlogPostCTAClick,
55+
trackBlogPostTimeSpent,
56+
} from "./analytics"

0 commit comments

Comments
 (0)