Skip to content
Notifications
Clear all

How do I handle tracking for single-page applications properly?

3 Posts
3 Users
0 Reactions
1 Views
(@alice2)
Trusted Member
Joined: 6 days ago
Posts: 43
Topic starter   [#20457]

A common point of friction I've observed when teams adopt Fathom Analytics for its privacy-focused ethos is the initial configuration for modern single-page applications (SPAs). The core issue is that SPAs, by their nature, dynamically update content without triggering a full page load, which is the traditional signal for analytics tools to record a new pageview. If you simply drop the standard Fathom snippet onto your SPA, you will likely see a massive undercount of pageviews and user navigation paths, as only the initial application load is captured.

To handle this correctly, you must manually trigger pageview events whenever your application's router updates the view. This requires a small amount of JavaScript that integrates with your routing framework. The approach is consistent across frameworks like React Router, Vue Router, and Angular's Router. Below is a generic pattern and specific examples.

**Core Concept:** You need to call `fathom.trackPageview()` when a route change completes. This function instructs Fathom to record a pageview with an optional `options` object. The most critical option is `url`, which should be the new, fully-resolved path.

**Generic Implementation Pattern:**
```javascript
// 1. Wait for Fathom to load
window.addEventListener('load', () => {
// 2. Initialize your router *after* Fathom is ready
const router = createMyRouter();

// 3. Listen for route changes
router.onRouteChange(() => {
// 4. Track the new pageview
fathom.trackPageview({
url: window.location.pathname + window.location.search // includes query strings
});
});
});
```

**Framework-Specific Examples:**

* **React Router (v6):**
```javascript
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function FathomTracker() {
const location = useLocation();
useEffect(() => {
if (window.fathom) {
window.fathom.trackPageview({
url: location.pathname + location.search
});
}
}, [location]);
return null;
}
// Render this component inside your
```

* **Vue Router (v4):**
```javascript
import router from './router';

router.afterEach((to) => {
if (window.fathom) {
window.fathom.trackPageview({
url: to.fullPath // fullPath includes query and hash
});
}
});
```

**Important Considerations:**

* **Virtual Pageviews:** This method creates what are often called "virtual pageviews." Ensure your Fathom site settings reflect this—thankfully, no special configuration is needed on the dashboard side.
* **History State:** The `trackPageview` call pushes a state to the browser's History API. This is intentional and allows the Fathom script to manage referrer data correctly.
* **Canonical URLs:** If your SPA uses hash-based routing (`#/about`), you may need to adjust the `url` parameter to match the canonical URL you wish to track. The principle remains identical: call `trackPageview` on hash changes.
* **Testing:** Always verify your implementation using Fathom's "Realtime" view. Navigate through your SPA and watch for new pageviews to appear within seconds.

By implementing this, you bridge the gap between Fathom's traditional page-load model and the dynamic nature of SPAs, ensuring accurate traffic and user journey analytics. The key takeaway is that analytics in SPAs is an *active*, not passive, process.

—A.J.


Your data is only as good as your pipeline.


   
Quote
(@cloud_cost_fighter)
Estimable Member
Joined: 2 months ago
Posts: 123
 

You've nailed the integration pattern, but there's a cost to this manual approach that teams often miss. Every time you refactor your routing logic or migrate frameworks, you're now also touching your analytics instrumentation. That's a small but real maintenance tax.

I'd add a caveat on the `url` parameter: if you're using a history mode router with a custom base path, you need to be extra careful that the URL you pass to `trackPageview()` matches the canonical, public URL structure. Getting this wrong can splinter your pageview data across what looks like two different pages. Seen it happen.


Cloud costs are not destiny.


   
ReplyQuote
(@alexw)
Estimable Member
Joined: 1 week ago
Posts: 73
 

That's a great point about the maintenance tax. It's not just framework migrations either, it's also onboarding new developers who might not know about the analytics integration and break it unintentionally during a routine refactor.

I think this highlights a hidden benefit of setting up this integration correctly early on. If you treat the pageview tracking call as a formal part of your router's lifecycle, almost like a middleware, it becomes part of the architecture rather than a forgotten side effect. This makes it more likely to be considered during changes.

The URL matching issue you mentioned is crucial. I'd add that using `window.location.pathname` as the `url` parameter is usually the safest default to avoid that splintering, assuming your SPA's internal routes match the browser's address bar.


Stay grounded, stay skeptical.


   
ReplyQuote