Fixing Navigation and Analytics: When Your Data Lies About User Behavior

htmx web-components analytics javascript
Fixing shadow-DOM navigation and back/forward history with HTMX

I had a classic small-team problem: the site felt great to use, but the data said nobody was reading more than one page per session. The data was wrong. Here's what I found and how I fixed it in an evening.

If you want the broader architectural context for why this site uses fragments and progressive enhancement, start with How This Blog Works and the HAL deep dive.

The real problem: bad data leads to bad decisions

Analytics showed one page view per session, even for readers who were clearly browsing multiple articles. If I'd taken that data at face value, I might have concluded the content wasn't engaging - and made content decisions based on a lie. The root cause was purely technical: HTMX fragment swaps weren't firing page_view events.

This is a pattern I see constantly in small product teams. You build something that works well for users, instrument it naively, then make strategic decisions based on metrics that don't reflect reality. Fixing the instrumentation is often higher-leverage than fixing the product.

What I shipped

1. Nav clicks now behave like article navigation

Article cards already worked correctly - they live in the normal DOM where HTMX can handle fragment swaps and URL pushes. But the site navigation lives inside PrimaryLayout's shadow DOM, which created two concrete bugs:

The fix: PrimaryLayout now intercepts nav clicks, performs the fragment load into #main-content explicitly, and pushes the URL. The result is what users expect:

This also aligns with the mobile UX work I described in Designing Mobile-First Reading Experiences with PrimaryLayout.

2. Analytics now tracks what readers actually do

The fix was straightforward:

Each virtual page view includes fromUrl and toUrl metadata, so I can reconstruct actual reading paths within a session. That's far more useful for content strategy than raw page counts.

The full analytics pipeline - Netlify Functions, Blob Store, GitHub snapshots - is documented in Zero-Server Analytics.

3. Manual rollups are safer

One operational improvement: manual analytics rollups now hit a dedicated endpoint that runs the same logic as the scheduled job without depending on scheduler-specific wrapper behavior. That means I can debug rollups in production without risking the daily scheduled job. Small thing, but it removes a class of "am I going to break my data pipeline?" anxiety when troubleshooting.

Why this matters

These are the kinds of fixes that don't show up in a feature changelog but have outsized impact on decision-making:

The total cost of these fixes was one evening of work and zero new dependencies. For a solo operator, that's the kind of leverage ratio that matters.

Next up

Recommended

Mobile-First Layout That Ships: How PrimaryLayout Solves Real UX Problems @jameslcowanjun 6 '26 htmx, css
How This Site Works: Architecture for a One-Person Team @jameslcowanjun 8 '26 htmx, javascript
HAL: Cutting 100-300KB of JavaScript by Moving Routing to Build Time @jameslcowanjun 10 '26 htmx, javascript
HAL: Build-Time Link Rewriting That Makes Navigation Feel Instant @jameslcowanjun 18 '26 htmx, javascript
Progressive Enhancement with HTMX: Ship Fast, Degrade Gracefully @jameslcowanjun 8 '26 htmx, javascript
Web Components + HTMX: A Lean Architecture for Content Sites That Ship @jameslcowanjun 10 '26 web-components, javascript
Why I Built My Own Analytics Pipeline (And What It Actually Costs) @jameslcowanjun 18 '26 analytics, javascript
Web Components as a Business Decision @jameslcowanjun 11 '26 web-components, javascript
Less JavaScript, More Leverage: Why I Ship With a 35KB Budget @jameslcowanjun 22 '26 javascript, performance
OpenClaw: What It Is and How to Get the Most Out of It @jameslcowanjun 5 '26 openclaw, javascript
Documentation That Scales: Constitution, Contracts, and Runbooks @jameslcowanjun 8 '26 documentation, markdown
Zero-Server Analytics: How I Replaced a SaaS Bill with Netlify Functions and GitHub @jameslcowanjun 8 '26 netlify, analytics
The Static Site Playbook: Shipping a Content Product on a Near-Zero Budget @jameslcowanjun 11 '26 static-site-generation, javascript
Anthropic Trained Its Replacement @jameslcowanjun 9 '26 anthropic, ai
Karpathy Was Wrong: OpenClaw Still Outruns Its 5 Real Alternatives @jameslcowanjun 11 '26 openclaw, ai
Pydantic: The Open Source Layer Quietly Running the AI Economy @jameslcowanjun 22 '26 pydantic, python
The YC S26 Deadline Just Closed. Here's What Separates the 1.5% From Everyone Else. @jameslcowanjun 17 '26 startups, yc
OpenClaw 2026.2.23: The Agent Browser Upgrade @jameslcowanjun 19 '26 openclaw, ai
Why I Skipped the CMS @jameslcowanjun 10 '26 markdown, static-site-generation
Static Site Generation: The Business Case for Pre-Rendered HTML @jameslcowanjun 14 '26 static-site-generation, performance

Recommended

Mobile-First Layout That Ships: How PrimaryLayout Solves Real UX Problems @jameslcowanjun 6 '26 htmx, css
How This Site Works: Architecture for a One-Person Team @jameslcowanjun 8 '26 htmx, javascript
HAL: Cutting 100-300KB of JavaScript by Moving Routing to Build Time @jameslcowanjun 10 '26 htmx, javascript
HAL: Build-Time Link Rewriting That Makes Navigation Feel Instant @jameslcowanjun 18 '26 htmx, javascript
Progressive Enhancement with HTMX: Ship Fast, Degrade Gracefully @jameslcowanjun 8 '26 htmx, javascript
Web Components + HTMX: A Lean Architecture for Content Sites That Ship @jameslcowanjun 10 '26 web-components, javascript
Why I Built My Own Analytics Pipeline (And What It Actually Costs) @jameslcowanjun 18 '26 analytics, javascript
Web Components as a Business Decision @jameslcowanjun 11 '26 web-components, javascript
Less JavaScript, More Leverage: Why I Ship With a 35KB Budget @jameslcowanjun 22 '26 javascript, performance
OpenClaw: What It Is and How to Get the Most Out of It @jameslcowanjun 5 '26 openclaw, javascript
Documentation That Scales: Constitution, Contracts, and Runbooks @jameslcowanjun 8 '26 documentation, markdown
Zero-Server Analytics: How I Replaced a SaaS Bill with Netlify Functions and GitHub @jameslcowanjun 8 '26 netlify, analytics
The Static Site Playbook: Shipping a Content Product on a Near-Zero Budget @jameslcowanjun 11 '26 static-site-generation, javascript
Anthropic Trained Its Replacement @jameslcowanjun 9 '26 anthropic, ai
Karpathy Was Wrong: OpenClaw Still Outruns Its 5 Real Alternatives @jameslcowanjun 11 '26 openclaw, ai
Pydantic: The Open Source Layer Quietly Running the AI Economy @jameslcowanjun 22 '26 pydantic, python
The YC S26 Deadline Just Closed. Here's What Separates the 1.5% From Everyone Else. @jameslcowanjun 17 '26 startups, yc
OpenClaw 2026.2.23: The Agent Browser Upgrade @jameslcowanjun 19 '26 openclaw, ai
Why I Skipped the CMS @jameslcowanjun 10 '26 markdown, static-site-generation
Static Site Generation: The Business Case for Pre-Rendered HTML @jameslcowanjun 14 '26 static-site-generation, performance