Skip to content

Commit 40fefc3

Browse files
joelhelblingclaude
andcommitted
feat: Add minimalytics analytics to website
Add lightweight analytics tracking using minimalytics hosted at minimalytics.hshp.cc. Tracks: - Page views for all pages (home, docs hub, individual doc pages) - Section scroll visibility on homepage (Why Glovebox, Features, Quick Start, Is This For Me?) - CTA clicks (Get Started button) - GitHub link clicks (header and footer) - Code block copy events with specific identifiers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 009c124 commit 40fefc3

6 files changed

Lines changed: 130 additions & 13 deletions

File tree

website/src/components/CodeBlock.astro

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ interface Props {
33
code: string;
44
lang?: string;
55
showPrompt?: boolean;
6+
trackingId?: string;
67
}
78
8-
const { code, lang = 'bash', showPrompt = true } = Astro.props;
9+
const { code, lang = 'bash', showPrompt = true, trackingId } = Astro.props;
910
1011
// Process lines to add prompt styling
1112
const lines = code.trim().split('\n');
1213
---
1314

1415
<div class="code-block-wrapper">
15-
<button type="button" class="copy-btn" aria-label="Copy to clipboard" data-code={code.trim()}>
16+
<button type="button" class="copy-btn" aria-label="Copy to clipboard" data-code={code.trim()} data-tracking-id={trackingId}>
1617
<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1718
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
1819
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
@@ -81,11 +82,16 @@ const lines = code.trim().split('\n');
8182
btn.addEventListener('click', async (e) => {
8283
e.preventDefault();
8384
const code = btn.getAttribute('data-code');
85+
const trackingId = btn.getAttribute('data-tracking-id');
8486
if (code) {
8587
try {
8688
await navigator.clipboard.writeText(code);
8789
btn.classList.add('copied');
8890
setTimeout(() => btn.classList.remove('copied'), 2000);
91+
// Track copy event if trackingId is set
92+
if (trackingId && window.trackEvent) {
93+
window.trackEvent('copy_' + trackingId);
94+
}
8995
} catch (err) {
9096
console.error('Failed to copy:', err);
9197
}

website/src/components/Footer.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
88
<!-- Links -->
99
<nav class="flex items-center gap-6 text-sm">
10-
<a href="https://github.com/joelhelbling/glovebox" class="text-muted hover:text-ink transition-colors">
10+
<a href="https://github.com/joelhelbling/glovebox" class="text-muted hover:text-ink transition-colors" data-track-click="github_footer">
1111
GitHub
1212
</a>
1313
<span class="text-muted/50">&middot;</span>

website/src/components/Header.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
const navLinks = [
33
{ href: '/docs/', label: 'Docs' },
4-
{ href: 'https://github.com/joelhelbling/glovebox', label: 'GitHub', external: true },
4+
{ href: 'https://github.com/joelhelbling/glovebox', label: 'GitHub', external: true, trackClick: 'github_header' },
55
];
66
---
77

@@ -24,6 +24,7 @@ const navLinks = [
2424
href={link.href}
2525
class="text-muted hover:text-ink transition-colors font-medium"
2626
{...(link.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
27+
{...(link.trackClick ? { 'data-track-click': link.trackClick } : {})}
2728
>
2829
{link.label}
2930
{link.external && (

website/src/components/Prose.astro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,11 @@
130130

131131
<script>
132132
function initProseCopyButtons() {
133+
let codeIndex = 0;
133134
document.querySelectorAll('.prose-docs pre').forEach(pre => {
134135
// Skip if already wrapped
135136
if (pre.parentElement?.classList.contains('code-wrapper')) return;
137+
const currentIndex = codeIndex++; // Capture current index for this block
136138

137139
// Create wrapper
138140
const wrapper = document.createElement('div');
@@ -143,6 +145,7 @@
143145
btn.type = 'button';
144146
btn.className = 'prose-copy-btn';
145147
btn.setAttribute('aria-label', 'Copy to clipboard');
148+
btn.setAttribute('data-code-index', currentIndex.toString());
146149
btn.innerHTML = `
147150
<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
148151
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
@@ -162,6 +165,13 @@
162165
await navigator.clipboard.writeText(code);
163166
btn.classList.add('copied');
164167
setTimeout(() => btn.classList.remove('copied'), 2000);
168+
// Track copy event for docs
169+
if (window.trackEvent) {
170+
const path = window.location.pathname;
171+
const pageName = path.replace('/docs/', '').replace(/\/$/, '') || 'index';
172+
const codeIndex = btn.getAttribute('data-code-index') || '0';
173+
window.trackEvent('copy_docs_' + pageName + '_' + codeIndex);
174+
}
165175
} catch (err) {
166176
console.error('Failed to copy:', err);
167177
}

website/src/layouts/BaseLayout.astro

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,105 @@ const fullTitle = title === 'Glovebox' ? title : `${title} | Glovebox`;
3535
<slot />
3636
</main>
3737
<Footer />
38+
39+
<script is:inline>
40+
// Analytics helper - sends events to minimalytics
41+
window.trackEvent = async function(eventName) {
42+
try {
43+
await fetch('https://minimalytics.hshp.cc/api/event/', {
44+
method: 'POST',
45+
headers: { 'Content-Type': 'application/json' },
46+
body: JSON.stringify({ event: eventName })
47+
});
48+
} catch (e) {
49+
// Silent fail - don't break site if analytics is down
50+
}
51+
};
52+
53+
// Track page view on load
54+
function trackPageView() {
55+
const path = window.location.pathname;
56+
let eventName;
57+
58+
if (path === '/') {
59+
eventName = 'pageview_home';
60+
} else if (path === '/docs/' || path === '/docs') {
61+
eventName = 'pageview_docs';
62+
} else if (path.startsWith('/docs/')) {
63+
// Extract doc page name: /docs/getting-started/ -> getting-started
64+
const pageName = path.replace('/docs/', '').replace(/\/$/, '');
65+
eventName = 'pageview_docs_' + pageName;
66+
} else {
67+
eventName = 'pageview_' + path.replace(/\//g, '_').replace(/^_|_$/g, '');
68+
}
69+
70+
window.trackEvent(eventName);
71+
}
72+
73+
// Track section visibility on homepage
74+
function initSectionTracking() {
75+
if (window.location.pathname !== '/') return;
76+
77+
const trackedSections = new Set();
78+
const sections = document.querySelectorAll('[data-track-section]');
79+
80+
if (sections.length === 0) return;
81+
82+
const observer = new IntersectionObserver((entries) => {
83+
entries.forEach((entry) => {
84+
if (entry.isIntersecting) {
85+
const sectionName = entry.target.dataset.trackSection;
86+
if (!trackedSections.has(sectionName)) {
87+
trackedSections.add(sectionName);
88+
window.trackEvent('scroll_home_' + sectionName);
89+
}
90+
}
91+
});
92+
}, { threshold: 0.3 });
93+
94+
sections.forEach((section) => observer.observe(section));
95+
}
96+
97+
// Track CTA and external link clicks
98+
function initClickTracking() {
99+
// CTA buttons
100+
document.querySelectorAll('[data-track-cta]').forEach((el) => {
101+
if (el.hasAttribute('data-tracking-initialized')) return;
102+
el.setAttribute('data-tracking-initialized', 'true');
103+
el.addEventListener('click', () => {
104+
window.trackEvent('cta_' + el.dataset.trackCta);
105+
});
106+
});
107+
108+
// External links (GitHub)
109+
document.querySelectorAll('[data-track-click]').forEach((el) => {
110+
if (el.hasAttribute('data-tracking-initialized')) return;
111+
el.setAttribute('data-tracking-initialized', 'true');
112+
el.addEventListener('click', () => {
113+
window.trackEvent('click_' + el.dataset.trackClick);
114+
});
115+
});
116+
}
117+
118+
// Initialize tracking
119+
if (document.readyState === 'loading') {
120+
document.addEventListener('DOMContentLoaded', () => {
121+
trackPageView();
122+
initSectionTracking();
123+
initClickTracking();
124+
});
125+
} else {
126+
trackPageView();
127+
initSectionTracking();
128+
initClickTracking();
129+
}
130+
131+
// Handle Astro page transitions
132+
document.addEventListener('astro:page-load', () => {
133+
trackPageView();
134+
initSectionTracking();
135+
initClickTracking();
136+
});
137+
</script>
38138
</body>
39139
</html>

website/src/pages/index.astro

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ import SectionHeader from '../components/SectionHeader.astro';
3030
</p>
3131

3232
<div class="mb-8 max-w-lg mx-auto">
33-
<CodeBlock code="brew install joelhelbling/glovebox/glovebox" />
33+
<CodeBlock code="brew install joelhelbling/glovebox/glovebox" trackingId="brew_install" />
3434
</div>
3535

36-
<a href="/docs/getting-started/" class="btn-primary">
36+
<a href="/docs/getting-started/" class="btn-primary" data-track-cta="get_started">
3737
Get Started
3838
<svg class="w-5 h-5 ml-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3939
<path d="M5 12h14M12 5l7 7-7 7"/>
@@ -44,7 +44,7 @@ import SectionHeader from '../components/SectionHeader.astro';
4444
</section>
4545

4646
<!-- Why Glovebox Section -->
47-
<section class="py-20 lg:py-24">
47+
<section class="py-20 lg:py-24" data-track-section="why_glovebox">
4848
<div class="container-narrow">
4949
<SectionHeader title="Why Glovebox" />
5050

@@ -69,7 +69,7 @@ import SectionHeader from '../components/SectionHeader.astro';
6969
</section>
7070

7171
<!-- Features Section -->
72-
<section class="py-20 lg:py-24 bg-white/50">
72+
<section class="py-20 lg:py-24 bg-white/50" data-track-section="features">
7373
<div class="container-wide">
7474
<SectionHeader title="Features" />
7575

@@ -99,36 +99,36 @@ import SectionHeader from '../components/SectionHeader.astro';
9999
</section>
100100

101101
<!-- Quick Start Section -->
102-
<section class="py-20 lg:py-24">
102+
<section class="py-20 lg:py-24" data-track-section="quickstart">
103103
<div class="container-narrow">
104104
<SectionHeader title="Quick Start" />
105105

106106
<div class="space-y-8">
107107
<div>
108108
<h3 class="text-lg font-medium mb-4">One-time setup:</h3>
109109
<CodeBlock code={`glovebox init --base # Select your OS, shell, editor, tools
110-
glovebox build --base # Build the base image`} />
110+
glovebox build --base # Build the base image`} trackingId="init_build_base" />
111111
</div>
112112

113113
<div>
114114
<h3 class="text-lg font-medium mb-4">Then, in any project:</h3>
115115
<CodeBlock code={`cd ~/projects/my-app
116-
glovebox run`} />
116+
glovebox run`} trackingId="run_project" />
117117
<p class="mt-4 text-muted">
118118
You're inside a sandboxed container. Your project is mounted at <code class="text-sm bg-black/5 px-1.5 py-0.5 rounded font-mono">/workspace</code>. Your shell, your editor, your tools—all there. When you exit, your container persists. When you return, it's waiting.
119119
</p>
120120
</div>
121121

122122
<div>
123123
<h3 class="text-lg font-medium mb-4">Clean up when needed:</h3>
124-
<CodeBlock code="glovebox clean --all" />
124+
<CodeBlock code="glovebox clean --all" trackingId="clean_all" />
125125
</div>
126126
</div>
127127
</div>
128128
</section>
129129

130130
<!-- Is This For Me Section -->
131-
<section class="py-20 lg:py-24 bg-white/50">
131+
<section class="py-20 lg:py-24 bg-white/50" data-track-section="is_this_for_me">
132132
<div class="container-narrow">
133133
<SectionHeader title="Is This For Me?" />
134134

0 commit comments

Comments
 (0)