How to Set Up Fathom Analytics in Your Web Apps

How to Set Up Fathom Analytics in Your Web Apps

Fathom Analytics can start with one script tag. That is part of its charm. But a useful analytics setup still needs engineering judgment: where the script loads, how client-side routes become pageviews, which actions deserve events, and how to keep your own staging traffic out of production reports.

The goal is not to measure everything.

The goal is to measure the few things you will actually use.

Also, if you are trying Fathom for the first time, this Fathom link should take $10 off your first invoice.

Start With The Standard Script

For a server-rendered site, static site, documentation site, or mostly traditional web app, the basic install is enough:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" defer></script>

Replace ABCDEFG with your Fathom site ID.

Put the snippet in the shared document head or layout that every public page uses. Keep it with app-level infrastructure, not scattered across individual pages.

In a Vite project, that usually means the base index.html plus a small analytics module near the rest of your app wiring. It is the same practical instinct behind maintaining a clear Vite folder structure: boring placement now saves debugging later.

Leave defer in place. Fathom documents this as the common script shape, and the script should not block the initial HTML parse.

Match Tracking To Your Router

The first real decision is routing.

If your site performs full page loads, the default script can track pageviews automatically. If your app handles navigation on the client, you need SPA mode or programmatic pageview calls.

For a simple SPA, start with Fathom's generic mode:

<script
  src="https://cdn.usefathom.com/script.js"
  data-site="ABCDEFG"
  data-spa="auto"
  defer
></script>

Fathom also supports explicit router modes:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-spa="history" defer></script>
<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-spa="hash" defer></script>

Use history for History API routing. Use hash for hash routing. Use auto when the generic detection is enough.

When you need more control, one option is fathom-client:

npm install fathom-client

fathom-client is a small community package maintained at derrickreimer/fathom-client that Fathom references in its React integration docs.

That makes it useful, but it is still an extra dependency. If data-spa="auto" handles your routing cleanly, you may not need it.

Here is the pattern I prefer in a React Router app:

import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import * as Fathom from "fathom-client";

export function FathomAnalytics() {
  const location = useLocation();

  useEffect(() => {
    Fathom.load(import.meta.env.VITE_FATHOM_SITE_ID, {
      auto: false,
    });
  }, []);

  useEffect(() => {
    Fathom.trackPageview({
      url: location.pathname + location.search,
      referrer: document.referrer,
    });
  }, [location.pathname, location.search]);

  return null;
}

The key is auto: false. You load the script once, then you decide when pageviews fire. That avoids the classic SPA bug where the first page gets counted once automatically and again when your route effect runs.

If the site ID comes from Vite, keep it in a public env var such as VITE_FATHOM_SITE_ID. Treat it like the rest of your client configuration: obvious, documented, and easy to review.

The same habits you use to configure your Vite config file apply here too.

Use Events For Intent

Pageviews tell you what people opened. Events tell you what people did.

Fathom's current event API looks like this:

fathom.trackEvent("newsletter signup");

For a monetary value, pass _value in cents:

fathom.trackEvent("purchase completed", {
  _value: 9900,
});

That means $99.00, not $9,900.

Be selective. A click is not automatically worth tracking. Track actions that could change a product, content, or marketing decision:

  • newsletter signup
  • checkout completed
  • trial started
  • contact form submitted
  • docs search used
  • pricing CTA clicked

Keep names plain. Fathom's docs recommend recognizable event names, and there is a practical reason: once an event is created and fired into the dashboard, you cannot rename that event in place. You can change what your code sends later, but that creates a new event line.

Also, avoid stale examples that use trackGoal for new work. Fathom says trackGoal is no longer supported for new goals/events as of October 25, 2023. Use trackEvent.

Hide The Vendor Behind A Tiny Module

Do not wire analytics calls directly into every component.

Create a small app-owned wrapper:

import { trackEvent } from "fathom-client";

export function trackSignup() {
  trackEvent("newsletter signup");
}

export function trackPurchase(totalInCents: number) {
  trackEvent("purchase completed", {
    _value: totalInCents,
  });
}

Then your UI code calls your app's intent:

function SignupForm() {
  async function onSubmit() {
    await submitSignup();
    trackSignup();
  }

  return <form onSubmit={onSubmit}>{/* fields */}</form>;
}

That wrapper gives you one place to rename events, add test guards, disable analytics in certain environments, or move vendors later. It also keeps your components from becoming a thin layer around reporting code.

In a Next.js app, keep this client analytics code away from server API route organization. It belongs with app shell and client instrumentation. If those boundaries are already fuzzy, a quick pass over your Next.js API folder structure can save you from mixing browser analytics with server endpoints.

Keep Staging Traffic Out

This is the part people skip, and it quietly ruins the dashboard.

You add the script. You open localhost. You test a preview deployment. A teammate clicks around. Suddenly the dashboard says your new page is getting traffic.

It is not traffic. It is you.

Use Fathom's Firewall settings to keep non-production hits out. The most useful options are:

  • Allowed domains
  • Blocked IPs
  • Blocked pages
  • Blocked referrers

For most sites, allowed domains should be the default control. If the production site is example.com, allow only that domain and any production subdomains you actually use.

Watch the wildcard behavior:

*.example.com
example.com

Fathom documents that *.example.com covers subdomains such as www.example.com, but not the root example.com. If you need both, list both.

Also remember that firewall changes can take up to 10 minutes to apply. Give the dashboard a little time before you decide an exclusion failed.

Reach For Advanced Attributes Carefully

The default script should be your starting point. Advanced attributes are for specific problems.

Disable automatic pageview tracking when you want manual control:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-auto="false" defer></script>

Ignore canonical URLs when canonical tags are causing the wrong path to be reported:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-canonical="false" defer></script>

Honor Do Not Track if that is your policy:

<script src="https://cdn.usefathom.com/script.js" data-site="ABCDEFG" data-honor-dnt="true" defer></script>

Fathom also says its embed script does not use cookies or similar technologies, and that EU Isolation is enabled by default for customers. If you specifically need Extreme EU Isolation, Fathom documents this script host:

<script src="https://cdn-eu.usefathom.com/script.js" data-site="ABCDEFG" defer></script>

Treat that as a compliance and infrastructure choice, not something to toggle casually during a cleanup.

Verify Before You Trust The Chart

"The script exists in the HTML" is not verification.

Check the whole path:

  1. The browser network tab loads the Fathom script successfully.
  2. A real production visit appears in Fathom's real-time dashboard.
  3. A localhost or staging visit does not appear after exclusions are active.
  4. A client-side route change creates one pageview, not zero and not two.
  5. A conversion event fires only after the real action succeeds.

That last point matters. If "purchase completed" fires when a success page mounts, a refresh can become a fake purchase. Fire events after the action is confirmed, not merely because a component rendered.

When you test across browsers, regions, or privacy settings, use the same slow and practical process you would use for browser location and timezone testing. Change one variable, predict the result, then check the dashboard.

The Takeaway

Fathom is a good fit when you want useful analytics without dragging a giant tracking stack into your app.

But simple tools still deserve careful installation.

Install the script once. Match tracking to your router. Wrap events behind app-owned functions. Lock down allowed domains. Verify with real browser behavior before trusting the reports.

That is how you get analytics you can use instead of a clean-looking chart full of your own test traffic.

Sources

Walt is a software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20+ years.
No comments posted yet
// Add a comment
// Color Theme

Custom accent
Pick any color
for the accent