Skip to content

Commit a941c64

Browse files
authored
Merge pull request #562 from raifdmueller/feat/anchor-pageview-tracking
feat(analytics): count SPA pageviews incl. each opened anchor
2 parents 90ee697 + 57f7a13 commit a941c64

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

website/src/utils/router.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,23 @@ export function initRouter() {
142142
/**
143143
* Handle route change
144144
*/
145+
// SPA pageview tracking for GoatCounter. count.js auto-counts the initial full
146+
// page load, so the first handleRoute() is skipped to avoid a double hit; every
147+
// later client-side route change — including each opened anchor (/anchor/:id) —
148+
// is reported with its resolved path and title, giving per-anchor view counts.
149+
// Privacy is unchanged: path only, no query string, no personal data.
150+
let firstRouteHandled = false
151+
function trackPageview() {
152+
if (!firstRouteHandled) {
153+
firstRouteHandled = true
154+
return
155+
}
156+
const gc = typeof window !== 'undefined' ? window.goatcounter : null
157+
if (gc && typeof gc.count === 'function') {
158+
gc.count({ path: window.location.pathname, title: document.title })
159+
}
160+
}
161+
145162
function handleRoute() {
146163
let path = getCurrentRoute()
147164

@@ -176,6 +193,7 @@ function handleRoute() {
176193
// Set title to anchor name
177194
const readableName = safeAnchorId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
178195
document.title = `${readableName} — Semantic Anchors`
196+
trackPageview()
179197

180198
// Open the anchor modal as overlay on current page
181199
import('../components/anchor-modal.js').then(({ showAnchorDetails }) => {
@@ -190,6 +208,7 @@ function handleRoute() {
190208
currentRoute = path
191209
document.title = ROUTE_TITLES[path] || 'Semantic Anchors'
192210
handler()
211+
trackPageview()
193212
} else {
194213
// Default to home if route not found
195214
const homeHandler = routes.get('/')

website/src/utils/router.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,18 @@ describe('router', () => {
3535
window.dispatchEvent(new PopStateEvent('popstate'))
3636
expect(handler).toHaveBeenCalledTimes(2)
3737
})
38+
39+
it('reports a GoatCounter pageview with the path on SPA navigation', () => {
40+
window.goatcounter = { count: vi.fn() }
41+
addRoute('/gc-test', vi.fn())
42+
// The very first route of the run is skipped by design (count.js auto-counts
43+
// the initial load); clear and navigate again so we assert a later change.
44+
navigate('/gc-test')
45+
window.goatcounter.count.mockClear()
46+
navigate('/gc-test')
47+
expect(window.goatcounter.count).toHaveBeenCalledWith(
48+
expect.objectContaining({ path: '/gc-test' })
49+
)
50+
delete window.goatcounter
51+
})
3852
})

0 commit comments

Comments
 (0)