When I migrated this website from Next.js to SvelteKit, I had to figure out how to wire up Fathom Analytics. Fathom Analytics is an alternative to Google Analytics. It features a better user experience for website owners and is more privacy-friendly for visitors. This post expands on Matt Jennings's post How to use Fathom Analytics with SvelteKit.
Tracking page views and goals
There are usually two things you want to track with web analytics: page views and goals. Tracking page views helps you see how visitors move through your website, how long they spend on each page, and which pages are more popular than others. Tracking goals helps you track specific actions you would like your visitors to do, e.g., subscribe to your newsletter or click through to your Twitter profile. Actions typically involve clicking a link or a button. When such a click happens, a visitor has done what you wanted them to do, and you can track that you have accomplished your goal for this visitor.
Like any other analytics platform, Fathom requires a custom tracking script to be included in your website. This is straightforward for multipage applications: a visitor loads a page, and the script runs and records the page view. For single-page applications (SPAs), you need to put in additional work to ensure that client-side route changes are also tracked. Fathom lists common integrations in their docs, e.g., for Next.js or Gatsby, but not for SvelteKit.
Package fathom-client gives you full
control over triggering Fathom calls at various points in your SPA's page lifecycle. As Matt's
src/routes/+layout.svelte, the root layout component, is the place to
initialize the tracking script:
This code snippet uses Svelte's
onMount callback to load the tracking script
as soon as the layout component has been mounted. Whenever that is the case, the tracking script records
a page view.
Environment variables in SvelteKit
Your Fathom site ID must be exposed to the client for tracking to work. There is no harm in hard-wiring this ID in your code, but it is better to move it into an environment variable as shown here:
The above code snippet uses the module
to expose the variable
PUBLIC_FATHOM_SITE_ID to the client. Note that the environment
variable needs to be prefixed with
PUBLIC_ to be exposed to the client.
Dealing with ad-blockers
The tracking script is served from Fathom's domain in the previous code snippets. Since this is a predictable URL, ad-blockers block the tracking script. Fathom used to offer a way to serve the tracking script from a custom domain using a CNAME record. But in May 2023, they advised their customers to stop using custom domains:
You're getting this email because you created a custom domain at some point using Fathom Analytics.
We've had a few reports of custom domains not collecting data due to expired SSL certificates that our vendor isn't renewing automatically, which is unacceptable to us, and we take full responsibility for this.
We've tried to fix this issue with our vendor but aren't getting anywhere, so you need to stop using your Fathom custom domains right now.
The custom domain approach has always been a double-edged sword. On the one hand, as the website owner, you are interested in accurately tracking your visitors. On the other hand, by creating a CNAME record to serve a third-party tracking script, you delegate the reputation of your domain to that third party. If Fathom had gone rogue, they could have served a malicious tracking script from your domain, get access to secrets such as authentication cookies, and destroy trust in your domain. Fathom has always been upfront about this potential issue.
When you use Fathom's default tracking script
cdn.usefathom.com/script.js, any visits
to your website from within the EU will be processed and anonymized within the EU. This is called
EU isolation and
works out of the box with zero configuration.
Fathom also offers extreme EU isolation, where all global traffic is routed through the
EU. All you need to do is use
cdn-eu.usefathom.com/script.js as tracking script instead
of the default one.
You can provide an options object to
Fathom.load as the second argument. It has a
url, which defaults to Fathom's default tracking script. If you want extreme EU
isolation, you need to set the
url option to the alternative tracking script:
Tracking client-side route changes
This great hack from Matt's post took me a while to understand. The last line is a reactive
statement, but it is not the typical example where something is assigned to a variable. It uses
comma operator, which evaluates comma-separated operands in sequence and returns the value of the last operand.
$page.url.pathname is a reference to the current path in
SvelteKit's page store, and whenever
this value changes, it triggers the reactive statement. The last operand fires a
trackPageview, but only when SvelteKit runs in a browser.
Let's assume your goal is to make visitors click through to your Twitter profile. The following component can render social icons from an array of objects, which it receives as a prop:
on:click directive to which an optional callback can be assigned. You can
Fathom.trackGoal with a goal tracking ID in this callback:
You have to generate the goal-tracking ID in your Fathom dashboard.