Skip to content

Commit f0efba0

Browse files
authored
Merge branch 'main' into astro-v6
2 parents bc7b070 + 0efaab9 commit f0efba0

File tree

8 files changed

+329
-11
lines changed

8 files changed

+329
-11
lines changed

src/components/ExternalArticle.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ type FeaturedArticle = {
77
title?: string;
88
url?: string;
99
date?: Date;
10+
toolSlug?: string;
1011
} & Partial<OgObject>;
1112

1213
const ExternalArticle: React.FC<FeaturedArticle> = (props) => {
13-
const { url, ogImage, ogDate } = props;
14+
const { url, ogImage, ogDate, toolSlug } = props;
1415

1516
const title = props.title ?? props.ogTitle ?? props.twitterTitle;
1617

@@ -53,7 +54,13 @@ const ExternalArticle: React.FC<FeaturedArticle> = (props) => {
5354
</div>
5455
<div className="group relative">
5556
<h3 className="mt-3 text-lg leading-6 font-semibold text-gray-900 group-hover:text-gray-600">
56-
<Link href={url} className="font-medium no-underline">
57+
<Link
58+
href={url}
59+
className="font-medium no-underline"
60+
toolSlug={toolSlug}
61+
linkType="featured_article"
62+
linkPlacementDescription="featured-article-card"
63+
>
5764
<span className="absolute inset-0" />
5865
{title}
5966
</Link>

src/components/Link.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,30 @@ const SITE_URL = import.meta.env.PROD
88
? 'https://openapi.tools'
99
: 'http://localhost';
1010

11+
type LinkType =
12+
| 'website'
13+
| 'repo'
14+
| 'sponsor_banner'
15+
| 'featured_article'
16+
| 'sponsor_link'
17+
| 'other';
18+
1119
/**
1220
* The props for the Link component
1321
* @param {Category} category - The category of the tool
1422
* @param {string} linkPlacementDescription - The description of where the link is placed. This will be slugified and used as the utm_content parameter
1523
* @param {boolean} isSponsored - Whether this is a sponsored link (adds rel="sponsored" for FTC/Google compliance)
24+
* @param {string} toolSlug - The slug of the tool (for analytics)
25+
* @param {string} toolName - The name of the tool (for analytics)
26+
* @param {LinkType} linkType - The type of link (for analytics)
1627
*/
1728
type LinkProps = React.HTMLProps<HTMLAnchorElement> & {
1829
category?: Category;
1930
linkPlacementDescription?: string;
2031
isSponsored?: boolean;
32+
toolSlug?: string;
33+
toolName?: string;
34+
linkType?: LinkType;
2135
};
2236

2337
const Link: React.FC<LinkProps> = ({
@@ -28,6 +42,9 @@ const Link: React.FC<LinkProps> = ({
2842
category,
2943
linkPlacementDescription,
3044
isSponsored,
45+
toolSlug,
46+
toolName,
47+
linkType = 'other',
3148
...rest
3249
}) => {
3350
// Compute rel attribute for external links
@@ -52,14 +69,19 @@ const Link: React.FC<LinkProps> = ({
5269
(e: React.MouseEvent<HTMLAnchorElement>) => {
5370
const href = e.currentTarget.href;
5471

55-
// If the link is an outbound link, track it
72+
// If the link is an outbound link, track it with full context
5673
if (href.startsWith('http') && !href.startsWith(SITE_URL)) {
5774
posthog.capture('outbound_link_click', {
58-
href,
75+
url: href,
76+
tool_slug: toolSlug,
77+
tool_name: toolName,
78+
link_type: linkType,
79+
is_sponsored: isSponsored ?? false,
80+
placement: linkPlacementDescription ?? 'unknown',
5981
});
6082
}
6183
},
62-
[]
84+
[toolSlug, toolName, linkType, isSponsored, linkPlacementDescription]
6385
);
6486

6587
const updatedUrl = React.useMemo(() => {

src/components/SponsorBanner.astro

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if (!sponsor) {
2121
id="sponsor-banner"
2222
>
2323
<a
24+
id="sponsor-banner-link"
2425
href={generateUrlWithUTM({
2526
url: sponsor.ctaUrl,
2627
linkPlacementDescription: 'rotating sponsor banner',
@@ -43,9 +44,35 @@ if (!sponsor) {
4344
</div>
4445
</a>
4546
<a
47+
id="become-sponsor-link"
4648
class="flex items-center px-2 py-1 text-nowrap md:justify-end"
4749
href="/sponsor"
4850
>
4951
<span class="text-white">Sponsor openapi.tools</span>
5052
</a>
5153
</section>
54+
55+
<script is:inline define:vars={{ sponsorName: sponsor.name }}>
56+
document
57+
.getElementById('sponsor-banner-link')
58+
?.addEventListener('click', () => {
59+
if (window.posthog) {
60+
window.posthog.capture('sponsorship_cta_click', {
61+
cta_type: 'banner_click',
62+
source_page: window.location.pathname,
63+
sponsor_name: sponsorName,
64+
});
65+
}
66+
});
67+
68+
document
69+
.getElementById('become-sponsor-link')
70+
?.addEventListener('click', () => {
71+
if (window.posthog) {
72+
window.posthog.capture('sponsorship_cta_click', {
73+
cta_type: 'become_sponsor_link',
74+
source_page: window.location.pathname,
75+
});
76+
}
77+
});
78+
</script>

src/components/filters/FilterableToolsList.tsx

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useMemo } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import { X } from 'lucide-react';
3+
import posthog from 'posthog-js';
34

45
import type { ToolRowData } from '@/components/table/Columns';
56
import { DataTable } from '@/components/table/DataTable';
@@ -27,6 +28,51 @@ export function FilterableToolsList({ tools }: FilterableToolsListProps) {
2728
const availableLanguages = useMemo(() => extractLanguages(tools), [tools]);
2829
const availablePlatforms = useMemo(() => extractPlatforms(tools), [tools]);
2930

31+
// Determine filter type (language vs platform)
32+
const getFilterType = useCallback(
33+
(value: string): 'language' | 'platform' => {
34+
return availablePlatforms.some((p) => p.value === value)
35+
? 'platform'
36+
: 'language';
37+
},
38+
[availablePlatforms]
39+
);
40+
41+
// Track filter changes
42+
const handleToggleLanguage = useCallback(
43+
(value: string) => {
44+
const isRemoving = selectedLanguages.includes(value);
45+
46+
// Calculate what the filtered count will be after this change
47+
const newSelected = isRemoving
48+
? selectedLanguages.filter((l) => l !== value)
49+
: [...selectedLanguages, value];
50+
const newFilteredTools = filterToolsByLanguages(tools, newSelected);
51+
52+
posthog.capture('filter_applied', {
53+
filter_type: getFilterType(value),
54+
filter_value: value,
55+
action: isRemoving ? 'removed' : 'added',
56+
total_results: newFilteredTools.length,
57+
});
58+
59+
toggleLanguage(value);
60+
},
61+
[selectedLanguages, toggleLanguage, tools, getFilterType]
62+
);
63+
64+
const handleClearFilters = useCallback(() => {
65+
if (selectedLanguages.length > 0) {
66+
posthog.capture('filter_applied', {
67+
filter_type: 'language',
68+
filter_value: 'all',
69+
action: 'cleared',
70+
total_results: tools.length,
71+
});
72+
}
73+
clearFilters();
74+
}, [selectedLanguages.length, clearFilters, tools.length]);
75+
3076
// Filter tools based on selected languages (includes both languages and platforms)
3177
const filteredTools = useMemo(
3278
() => filterToolsByLanguages(tools, selectedLanguages),
@@ -58,13 +104,13 @@ export function FilterableToolsList({ tools }: FilterableToolsListProps) {
58104
title="Languages"
59105
options={availableLanguages}
60106
selectedValues={selectedLanguages}
61-
onToggle={toggleLanguage}
107+
onToggle={handleToggleLanguage}
62108
/>
63109
<FilterPopover
64110
title="Platforms"
65111
options={availablePlatforms}
66112
selectedValues={selectedLanguages}
67-
onToggle={toggleLanguage}
113+
onToggle={handleToggleLanguage}
68114
/>
69115

70116
{hasFilters && (
@@ -77,7 +123,7 @@ export function FilterableToolsList({ tools }: FilterableToolsListProps) {
77123
key={value}
78124
variant="secondary"
79125
className="cursor-pointer bg-slate-200 text-slate-700 hover:bg-slate-300 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
80-
onClick={() => toggleLanguage(value)}
126+
onClick={() => handleToggleLanguage(value)}
81127
>
82128
{option?.label || value}
83129
<X className="ml-1 h-3 w-3" />
@@ -88,7 +134,7 @@ export function FilterableToolsList({ tools }: FilterableToolsListProps) {
88134
<Button
89135
variant="ghost"
90136
size="sm"
91-
onClick={clearFilters}
137+
onClick={handleClearFilters}
92138
className="h-8 px-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
93139
>
94140
Clear all

src/layouts/PostHogLayout.astro

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,65 @@ import PostHog from '../components/PostHog.astro';
99
</head>
1010
<body>
1111
<slot />
12+
13+
{/* Scroll Depth Tracking */}
14+
<script is:inline>
15+
(function () {
16+
const thresholds = [25, 50, 75, 100];
17+
const firedThresholds = new Set();
18+
19+
function getScrollPercent() {
20+
const scrollTop =
21+
window.scrollY || document.documentElement.scrollTop;
22+
const scrollHeight =
23+
document.documentElement.scrollHeight -
24+
document.documentElement.clientHeight;
25+
if (scrollHeight === 0) return 100;
26+
return Math.round((scrollTop / scrollHeight) * 100);
27+
}
28+
29+
function getToolSlug() {
30+
const match = window.location.pathname.match(/^\/tools\/([^/]+)/);
31+
return match ? match[1] : undefined;
32+
}
33+
34+
function trackScrollDepth() {
35+
const percent = getScrollPercent();
36+
37+
for (const threshold of thresholds) {
38+
if (percent >= threshold && !firedThresholds.has(threshold)) {
39+
firedThresholds.add(threshold);
40+
41+
if (window.posthog) {
42+
window.posthog.capture('scroll_depth', {
43+
page_path: window.location.pathname,
44+
max_depth_percent: threshold,
45+
tool_slug: getToolSlug(),
46+
});
47+
}
48+
}
49+
}
50+
}
51+
52+
// Throttle scroll events
53+
let ticking = false;
54+
window.addEventListener(
55+
'scroll',
56+
function () {
57+
if (!ticking) {
58+
window.requestAnimationFrame(function () {
59+
trackScrollDepth();
60+
ticking = false;
61+
});
62+
ticking = true;
63+
}
64+
},
65+
{ passive: true }
66+
);
67+
68+
// Check initial scroll position (for pages that load scrolled)
69+
window.addEventListener('load', trackScrollDepth);
70+
})();
71+
</script>
1272
</body>
1373
</html>

src/pages/sponsor/index.astro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,14 @@ const uniqueSponsors = activeSponsors.filter((tool) => {
182182
</p>
183183
</div>
184184
</Layout>
185+
186+
{/* Sponsor Page Visit Tracking */}
187+
<script is:inline>
188+
if (typeof window !== 'undefined' && window.posthog) {
189+
window.posthog.capture('sponsorship_cta_click', {
190+
cta_type: 'sponsor_page_visit',
191+
source_page: document.referrer || 'direct',
192+
});
193+
}
194+
</script>
185195
</PostHogLayout>

src/pages/tools/[...slug].astro

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ const toolBadges = tool.data.badges
103103
variant="primary"
104104
isSponsored={isSponsorshipActive(tool.data)}
105105
className="inline-flex items-center gap-2"
106+
toolSlug={slug as string}
107+
toolName={tool.data.name}
108+
linkType="website"
109+
linkPlacementDescription="tool-detail-page-header"
106110
>
107111
<WebsiteIcon className="h-4 w-4" />
108112
Visit Website
@@ -118,6 +122,10 @@ const toolBadges = tool.data.badges
118122
variant="secondary"
119123
isSponsored={isSponsorshipActive(tool.data)}
120124
className="inline-flex items-center gap-2"
125+
toolSlug={slug as string}
126+
toolName={tool.data.name}
127+
linkType="repo"
128+
linkPlacementDescription="tool-detail-page-header"
121129
>
122130
<RepoIcon className="h-4 w-4" repo={tool.data.repo} />
123131
View Repository
@@ -232,12 +240,34 @@ const toolBadges = tool.data.badges
232240

233241
<section class="md-grid-cols-2 grid gap-4 lg:grid-cols-3">
234242
{enrichedFeaturedArticles.map((article) => (
235-
<ExternalArticle {...article.og} />
243+
<ExternalArticle {...article.og} toolSlug={slug as string} />
236244
))}
237245
</section>
238246
</>
239247
)
240248
}
241249
</main>
242250
</Layout>
251+
252+
{/* Tool Page View Tracking */}
253+
<script
254+
is:inline
255+
define:vars={{
256+
toolSlug: slug,
257+
toolName: tool.data.name,
258+
categoryNames: categories.map((c) => c?.data.name).filter(Boolean),
259+
isSponsored: isSponsorshipActive(tool.data),
260+
oasVersions: tool.data.oasVersions || {},
261+
}}
262+
>
263+
if (typeof window !== 'undefined' && window.posthog) {
264+
window.posthog.capture('tool_page_view', {
265+
tool_slug: toolSlug,
266+
tool_name: toolName,
267+
categories: categoryNames,
268+
is_sponsored: isSponsored,
269+
oas_versions: oasVersions,
270+
});
271+
}
272+
</script>
243273
</PostHogLayout>

0 commit comments

Comments
 (0)