99## Problem
1010
1111After launching the portfolio and sharing on LinkedIn, we have zero visibility into:
12+
1213- ** Visitor behavior** - Are people reading the case study? Where do they drop off?
1314- ** Traffic sources** - Is LinkedIn driving traffic? Which posts work?
1415- ** Engagement patterns** - Do CTAs work? Which sections engage?
@@ -23,6 +24,7 @@ After launching the portfolio and sharing on LinkedIn, we have zero visibility i
2324** Chosen solution:** Umami Cloud analytics with custom event tracking
2425
2526** Why Umami:**
27+
2628- ✅ ** Free tier sufficient** (100K events/month, 3 websites, 6-month retention)
2729- ✅ ** Privacy-first** (cookie-free, GDPR compliant, no consent banner)
2830- ✅ ** Custom events** (scroll depth, CTA clicks, section engagement)
@@ -31,6 +33,7 @@ After launching the portfolio and sharing on LinkedIn, we have zero visibility i
3133- ✅ ** Lightweight** (~ 3KB script, async load, no performance impact)
3234
3335** Alternative considered (70% confident):** Cloudflare Web Analytics
36+
3437- ** Pros:** 100% free forever, even simpler setup
3538- ** Cons:** No custom event tracking (can't measure scroll depth, CTA clicks)
3639- ** Why rejected:** User wants "nice to have" event tracking for engagement insights
@@ -84,24 +87,26 @@ After launching the portfolio and sharing on LinkedIn, we have zero visibility i
8487### Tracking Strategy
8588
8689** Automatic tracking (no code needed):**
90+
8791- Pageviews, unique visitors
8892- Referrer sources (linkedin.com visible)
8993- Country, device, browser, OS
9094- Page paths and durations
9195
9296** Custom event tracking:**
9397
94- | Event Type | Trigger | Data Captured | Why |
95- | ------------| ---------| ---------------| -----|
96- | ` scroll-depth ` | Intersection Observer at 25/50/75/100% | page, depth, section | Shows where readers drop off in case study |
97- | ` cta-click ` | Button/Link click | location, text, destination | Measures CTA effectiveness |
98- | ` section-view ` | Intersection Observer on major sections | page, section-name | Identifies engaging vs skipped sections |
99- | ` outbound-link ` | External link click | destination, source-page | Tracks LinkedIn/GitHub profile clicks |
98+ | Event Type | Trigger | Data Captured | Why |
99+ | --------------- | --------------------------------------- | --------------------------- | ------------------------------------------ |
100+ | ` scroll-depth ` | Intersection Observer at 25/50/75/100% | page, depth, section | Shows where readers drop off in case study |
101+ | ` cta-click ` | Button/Link click | location, text, destination | Measures CTA effectiveness |
102+ | ` section-view ` | Intersection Observer on major sections | page, section-name | Identifies engaging vs skipped sections |
103+ | ` outbound-link ` | External link click | destination, source-page | Tracks LinkedIn/GitHub profile clicks |
100104
101105** UTM parameters for LinkedIn:**
106+
102107```
103- https://saad-shahd.dev /?utm_source=linkedin&utm_medium=social&utm_campaign=launch-2025-01
104- https://saad-shahd.dev /portfolio/statsbomb/?utm_source=linkedin&utm_medium=social&utm_campaign=case-study-statsbomb
108+ https://saadshahd.github.io /?utm_source=linkedin&utm_medium=social&utm_campaign=launch-2025-01
109+ https://saadshahd.github.io /portfolio/statsbomb/?utm_source=linkedin&utm_medium=social&utm_campaign=case-study-statsbomb
105110```
106111
107112---
@@ -111,16 +116,19 @@ https://saad-shahd.dev/portfolio/statsbomb/?utm_source=linkedin&utm_medium=socia
111116### Phase 1: Base Integration (5 minutes, 1 story point)
112117
113118** Files modified:**
119+
1141201 . ` src/layouts/Layout.astro ` - Add Umami script tag
1151212 . ` .env ` - Add ` PUBLIC_UMAMI_WEBSITE_ID ` (from Umami dashboard)
1161223 . ` .env.example ` - Document environment variable
117123
118124** Changes:**
125+
119126- Add conditional script tag in ` <head> ` (only loads if env var set)
120127- Async/defer loading (no performance impact)
121128- TypeScript globals for ` window.umami `
122129
123130** Verification:**
131+
124132- Load site, check Network tab for ` script.js ` from ` cloud.umami.is `
125133- Visit Umami dashboard, confirm pageview recorded
126134- Test on multiple pages, verify paths tracked correctly
@@ -130,22 +138,25 @@ https://saad-shahd.dev/portfolio/statsbomb/?utm_source=linkedin&utm_medium=socia
130138### Phase 2: Analytics Utility (10 minutes, 1 story point)
131139
132140** Files created:**
141+
1331421 . ` src/utils/analytics.ts ` - Event tracking helpers
1341432 . ` src/types/umami.d.ts ` - TypeScript definitions for Umami API
135144
136145** API design:**
146+
137147``` typescript
138148// Core tracking function
139- export function trackEvent(name : string , data ? : Record <string , any >): void
149+ export function trackEvent(name : string , data ? : Record <string , any >): void ;
140150
141151// Convenience helpers
142- export function trackCTAClick(location : string , text : string ): void
143- export function trackScrollDepth(page : string , depth : number ): void
144- export function trackSectionView(page : string , section : string ): void
145- export function trackOutboundLink(destination : string , source : string ): void
152+ export function trackCTAClick(location : string , text : string ): void ;
153+ export function trackScrollDepth(page : string , depth : number ): void ;
154+ export function trackSectionView(page : string , section : string ): void ;
155+ export function trackOutboundLink(destination : string , source : string ): void ;
146156```
147157
148158** Type safety:**
159+
149160``` typescript
150161// src/types/umami.d.ts
151162interface Window {
@@ -156,6 +167,7 @@ interface Window {
156167```
157168
158169** Error handling:**
170+
159171- Check ` typeof window !== 'undefined' ` (SSR safety)
160172- Check ` 'umami' in window ` (script loaded)
161173- Silent failure if Umami unavailable (graceful degradation)
@@ -165,11 +177,13 @@ interface Window {
165177### Phase 3: CTA Tracking (10 minutes, 1 story point)
166178
167179** Files modified:**
180+
1681811 . ` src/components/Button.astro ` - Add click tracking
1691822 . ` src/components/Link.astro ` - Add outbound link tracking
1701833 . ` src/components/CalloutCTA.astro ` - Track CTA interactions
171184
172185** Implementation pattern:**
186+
173187``` astro
174188---
175189// Button.astro
@@ -197,6 +211,7 @@ const isExternal = href.startsWith('http');
197211```
198212
199213** Tracked CTAs:**
214+
200215- Homepage: "Explore My Work", "Start a Conversation"
201216- Case study: "Read Full Case Study" cards
202217- About page: "Download Resume"
@@ -207,53 +222,60 @@ const isExternal = href.startsWith('http');
207222### Phase 4: Scroll Depth Tracking (15 minutes, 2 story points)
208223
209224** Files modified:**
225+
2102261 . ` src/pages/portfolio/statsbomb.astro ` - Add scroll observer
211227
212228** Implementation approach:**
229+
213230``` typescript
214231// Intersection Observer watching key sections
215232const observeScrollDepth = () => {
216233 const sections = [
217- { element: document.querySelector(' #tldr' ), depth: 25 },
218- { element: document.querySelector(' #architecture' ), depth: 50 },
219- { element: document.querySelector(' #impact' ), depth: 75 },
220- { element: document.querySelector(' footer' ), depth: 100 },
234+ { element: document .querySelector (" #tldr" ), depth: 25 },
235+ { element: document .querySelector (" #architecture" ), depth: 50 },
236+ { element: document .querySelector (" #impact" ), depth: 75 },
237+ { element: document .querySelector (" footer" ), depth: 100 },
221238 ];
222239
223- const observer = new IntersectionObserver((entries) => {
224- entries.forEach(entry => {
225- if (entry.isIntersecting) {
226- const section = sections.find(s => s.element === entry.target);
227- if (section) {
228- umami.track('scroll-depth', {
229- page: 'statsbomb-case-study',
230- depth: ` $ {section .depth }% ` ,
231- section: entry.target.id
232- });
233- observer.unobserve(entry.target); // Fire once per section
240+ const observer = new IntersectionObserver (
241+ (entries ) => {
242+ entries .forEach ((entry ) => {
243+ if (entry .isIntersecting ) {
244+ const section = sections .find ((s ) => s .element === entry .target );
245+ if (section ) {
246+ umami .track (" scroll-depth" , {
247+ page: " statsbomb-case-study" ,
248+ depth: ` ${section .depth }% ` ,
249+ section: entry .target .id ,
250+ });
251+ observer .unobserve (entry .target ); // Fire once per section
252+ }
234253 }
235- }
236- });
237- }, { threshold: 0.5 }); // Fire when 50% of section visible
254+ });
255+ },
256+ { threshold: 0.5 }
257+ ); // Fire when 50% of section visible
238258
239- sections.forEach(s => s.element && observer.observe(s.element));
259+ sections .forEach (( s ) => s .element && observer .observe (s .element ));
240260};
241261
242262// Run after page load
243- if (document.readyState === ' complete' ) {
263+ if (document .readyState === " complete" ) {
244264 observeScrollDepth ();
245265} else {
246- window.addEventListener(' load' , observeScrollDepth);
266+ window .addEventListener (" load" , observeScrollDepth );
247267}
248268```
249269
250270** Why Intersection Observer:**
271+
251272- More accurate than scroll listeners
252273- Better performance (browser-optimized)
253274- Fires once per section (no duplicate events)
254275- Respects ` prefers-reduced-motion ` (standard behavior)
255276
256277** Sections tracked:**
278+
2572791 . ** 25% (TL;DR)** - User started reading
2582802 . ** 50% (Architecture)** - User engaged with technical content
2592813 . ** 75% (Impact)** - User reading outcomes
@@ -266,12 +288,14 @@ if (document.readyState === 'complete') {
266288** Umami dashboard setup:**
267289
2682901 . ** Custom reports:**
291+
269292 - "LinkedIn Traffic" - Filter by ` utm_source=linkedin `
270293 - "Case Study Engagement" - Filter ` scroll-depth ` events on statsbomb page
271294 - "CTA Performance" - Filter ` cta-click ` events, group by text
272295 - "Drop-off Analysis" - Compare scroll depths (100% - 75% - 50% - 25%)
273296
2742972 . ** Goals (if available on free tier):**
298+
275299 - Goal: "Contact CTA Click"
276300 - Goal: "Full Case Study Read" (100% scroll depth)
277301
@@ -285,6 +309,7 @@ if (document.readyState === 'complete') {
285309### Why Not Google Analytics?
286310
287311** Rejected (40% confident GA would work):**
312+
288313- ❌ Cookie-based (requires consent banner)
289314- ❌ Privacy concerns (tracks users across sites)
290315- ❌ Overkill for portfolio (enterprise features unused)
@@ -294,6 +319,7 @@ if (document.readyState === 'complete') {
294319### Why Not Plausible? ($9/month)
295320
296321** Deferred (90% confident Plausible would be better, but cost matters):**
322+
297323- ❌ $9/month exceeds "free only" budget
298324- ✅ Automatic scroll depth tracking (would save implementation time)
299325- ✅ Better UX, Google Search Console integration
@@ -302,6 +328,7 @@ if (document.readyState === 'complete') {
302328### Environment Variable Strategy
303329
304330** Decision:** Use ` PUBLIC_UMAMI_WEBSITE_ID ` (public prefix)
331+
305332- ✅ Astro convention for client-side env vars
306333- ✅ Safe to expose (website ID is public in script tag anyway)
307334- ✅ Easy to set in GitHub Pages environment (if needed)
@@ -311,18 +338,21 @@ if (document.readyState === 'complete') {
311338## Privacy & Compliance
312339
313340** GDPR/CCPA compliance:**
341+
314342- ✅ No cookies (no consent banner required)
315343- ✅ No personal data collected (anonymous counts)
316344- ✅ No cross-site tracking
317345- ✅ Visitor IP addresses anonymized
318346
319347** Performance impact:**
348+
320349- Umami script: ~ 3KB gzipped
321350- Loads async (doesn't block render)
322351- Custom events: negligible (<1KB per page)
323352- ** Expected load time:** 2.1s → 2.1s (no change)
324353
325354** Accessibility:**
355+
326356- ✅ Tracking doesn't affect screen readers
327357- ✅ No visual changes to UI
328358- ✅ Works with JavaScript disabled (graceful degradation)
@@ -332,13 +362,15 @@ if (document.readyState === 'complete') {
332362## Success Metrics
333363
334364** After 1 week, we should know:**
365+
335366- ✅ Total visitors from LinkedIn post
336367- ✅ Bounce rate (% who leave immediately)
337368- ✅ Case study engagement (% reaching 50%, 75%, 100% depth)
338369- ✅ CTA click-through rate
339370- ✅ Top referrer sources
340371
341372** After 1 month, we can optimize:**
373+
342374- If 50% drop at 50% scroll → Middle sections need work
343375- If LinkedIn bounce rate >70% → Landing page needs improvement
344376- If CTA clicks <5% → CTA copy/placement needs adjustment
@@ -349,12 +381,14 @@ if (document.readyState === 'complete') {
349381## Reversibility
350382
351383** High reversibility (90% confident):**
384+
352385- Remove script tag from Layout.astro → Back to no tracking (5 minutes)
353386- Keep event tracking code → Works with other analytics (Plausible, Fathom)
354387- Export data from Umami → CSV download anytime
355388- Migrate to self-hosted Umami → Same tracking code, different endpoint
356389
357390** Migration path to paid solution:**
391+
3583921 . Keep Umami script tag
3593932 . Add Plausible script tag (runs in parallel)
3603943 . Compare data for 1-2 weeks
@@ -413,6 +447,7 @@ Phase 6: Documentation & Launch (0.5 story points)
413447## Future Enhancements (Not Now)
414448
415449** Consider later if traffic grows:**
450+
4164511 . ** Heatmaps** (requires paid service like Hotjar)
4174522 . ** Session replay** (Umami Pro tier $20/month or LogRocket)
4184533 . ** A/B testing** (requires custom implementation or tool)
@@ -444,19 +479,22 @@ Phase 6: Documentation & Launch (0.5 story points)
444479** Overall confidence:** 85%
445480
446481** Based on:**
482+
447483- Umami is proven solution (31K GitHub stars, used by thousands)
448484- Free tier covers expected traffic (100K events >> portfolio needs)
449485- Implementation is straightforward (well-documented API)
450486- Reversibility is high (remove script tag = done)
451487
452488** Key assumption that could invalidate this:**
489+
453490- If traffic exceeds 100K events/month → Upgrade to Pro ($20/month) or self-host
454491
455492** Alternative confidence:** Self-hosted Umami (75% confident) - More setup complexity but free forever
456493
457494---
458495
459496** Applied patterns:**
497+
460498- Library-First (using Umami vs building custom analytics)
461499- Progressive Disclosure (start with base tracking, add events incrementally)
462500- Make Illegal States Unrepresentable (TypeScript types prevent invalid event data)
0 commit comments