Track all link clicks (internal + external) in analytics; v1.1.33

Replaces outbound-only tracking with a useLinkClickTracking hook that
captures every <a> click on the site. Stores as link_click events keyed
by destination URL, with an internal flag. Admin analytics page gains a
Link Clicks table sorted by count, showing type (internal/external).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-08-10 09:11:53 -04:00
parent abb4c87c41
commit bf3154378b
7 changed files with 73 additions and 12 deletions
+1
View File
@@ -601,6 +601,7 @@ export interface AdminStats {
scrollDepth: Array<{ path: string; 25?: number; 50?: number; 75?: number; 90?: number }>
timeOnPage: Array<{ path: string; avgSeconds: number; count: number }>
topOutboundClicks: Array<{ url: string; count: number }>
topLinkClicks: Array<{ url: string; count: number; internal: boolean }>
topUTMSources: Array<{ source: string; count: number }>
topSearchQueries: Array<{ query: string; count: number }>
top404s: Array<{ path: string; count: number }>
+2 -2
View File
@@ -16,7 +16,7 @@ import { useGlobalSearch } from './hooks/useGlobalSearch'
import { GlobalSearch } from './components/GlobalSearch'
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
import './App.css'
import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useOutboundLinkTracking } from './analytics'
import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useLinkClickTracking } from './analytics'
const CONSENT_KEY = 'vbn_analytics_consent_choice'
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
@@ -2387,7 +2387,7 @@ export default function App() {
useScrollDepthTracking()
useTimeOnPage()
useUTMCapture()
useOutboundLinkTracking()
useLinkClickTracking()
useEffect(() => {
const TARGET = 'salvation'
+29 -8
View File
@@ -87,22 +87,43 @@ export function useUTMCapture() {
}, [])
}
// --- Outbound link clicks ---
export function useOutboundLinkTracking() {
// --- All link clicks (internal + external) ---
export function useLinkClickTracking() {
useEffect(() => {
function onClick(e: MouseEvent) {
if (!hasConsent()) return
const target = (e.target as HTMLElement).closest('a')
if (!target) return
const href = target.getAttribute('href') ?? ''
if (!href.startsWith('http') && !href.startsWith('//')) return
try {
const url = new URL(href)
if (url.hostname === window.location.hostname) return
sendEvent('outbound_click', { url: href, text: target.textContent?.trim().slice(0, 100) ?? '' })
} catch { /* ignore malformed */ }
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return
let url: string
let internal: boolean
if (href.startsWith('http') || href.startsWith('//')) {
try {
const parsed = new URL(href)
if (parsed.hostname === window.location.hostname) {
url = parsed.pathname
internal = true
} else {
url = href
internal = false
}
} catch { return }
} else if (href.startsWith('/')) {
url = href.split('?')[0]
internal = true
} else {
return
}
sendEvent('link_click', { url: url.slice(0, 500), internal })
}
document.addEventListener('click', onClick, { capture: true })
return () => document.removeEventListener('click', onClick, { capture: true })
}, [])
}
/** @deprecated Use useLinkClickTracking instead */
export function useOutboundLinkTracking() { useLinkClickTracking() }
+27
View File
@@ -663,6 +663,33 @@ export function AnalyticsPanel({
</div>
)}
{/* All link clicks */}
{stats.engagement.topLinkClicks?.length > 0 && (
<div style={{ marginBottom: '1.5rem' }}>
<h3 style={{ marginBottom: '0.5rem', fontSize: '0.9rem', color: '#b0a48c' }}>Link Clicks</h3>
<div className="admin-stats-table-wrap">
<table className="admin-stats-table">
<thead><tr><th>Destination</th><th>Type</th><th>Clicks</th></tr></thead>
<tbody>
{stats.engagement.topLinkClicks.map((row: { url: string; count: number; internal: boolean }) => (
<tr key={row.url}>
<td style={{ maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{row.internal
? <span style={{ color: '#c9a84c' }}>{row.url}</span>
: <a href={row.url} target="_blank" rel="noreferrer" style={{ color: '#c9a84c' }}>{row.url}</a>}
</td>
<td style={{ color: row.internal ? '#6fcf97' : '#9b9b9b', fontSize: '0.8rem' }}>
{row.internal ? 'internal' : 'external'}
</td>
<td>{row.count.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Outbound clicks */}
{stats.engagement.topOutboundClicks?.length > 0 && (
<div style={{ marginBottom: '1.5rem' }}>