How to wire up Fathom Analytics in a SvelteKit app
Last modified:
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 to your website. This post expands on Matt Jennings’s post How to use Fathom Analytics with SvelteKit.
Tracking pageviews and goals
There are usually two things you want to track with web analytics: pageviews and goals. Tracking pageviews helps you understand how visitors navigate 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 that 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 with any other analytics tool, Fathom requires a custom tracking script to be included in your website. This is straightforward for multi-page applications: a visitor loads a page and the script runs and records the pageview. 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
Package fathom-client gives you full control over
triggering Fathom calls at various points in your SPA’s page lifecycle. As Matt’s post suggests,
src/routes/__layout.svelte
, the default layout component, is the place to initialize the tracking
script:
<script>
import { onMount } from 'svelte';
import * as Fathom from 'fathom-client';
onMount(() => {
Fathom.load('FATHOM_SITE_ID', {
includedDomains: ['maier.tech'],
});
});
</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 pageview.
The second argument in load
is an
object of options. It has a property
url
, which defaults to Fathom’s tracking script https://cdn.usefathom.com/script.js. Property
includedDomains
tells the tracking script to track the production website only, which is served
from maier.tech
. Any other instances of the website, e.g. deploy previews, are not tracked.
Environment variables in SvelteKit
Your Fathom site ID needs to 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:
<script>
import { onMount } from 'svelte';
import * as Fathom from 'fathom-client';
onMount(() => {
Fathom.load(import.meta.env.VITE_FATHOM_SITE_ID, {});
});
</script>
Environment variables in SvelteKit actually
rely on Vite to expose them to the client.
Therefore, you need to access the environment variable via import.meta.env
and not
process.env
. Vite will expose any environment variables defined in the build environment
that start with VITE_
.
Serving the tracking script from a custom domain
In previous code snippets, the tracking script is served from Fathom’s domain. This means that sooner than later the tracking script will be blocked by ad-blockers. Fathom offers a way to serve the tracking script from a custom domain using a CNAME record. Serving the tracking script from the same domain as the tracked website increases the chances that the tracking script is not blocked by ad-blockers or other browser security mechanisms. I will not go into the details of how this is done, because you can read up on it here in the Fathom docs.
This approach is a double-edged sword. On the one hand, you as the website owner have an interest in accurately tracking your visitors to improve your site and serve them better. On the other hand, by creating that CNAME record, you delegate the reputation of your domain to a third party. If Fathom went rogue, they could serve a malicious tracking script from your domain, get access to secrets such as authentication cookies and thereby destroy trust in your domain. Fathom is upfront about this potential issue. They make a strong case for why you should trust them in this post.
If you choose to serve your tracking script from your custom domain, you need to set the url
option, as shown below, using the custom tracking script URL provided by Fathom. While you are at
it, you should also set the honorDNT
option to true. DNT refers to the “Do Not Track” request
header, which is
officially deprecated, but still
supported in
Chrome
and Firefox. This gives
visitors to your website a way to completely opt out of tracking.
<script>
import { onMount } from 'svelte';
import * as Fathom from 'fathom-client';
onMount(() => {
Fathom.load(import.meta.env.VITE_FATHOM_SITE_ID, {
url: 'https://firefly.maier.tech/script.js',
honorDNT: true,
});
});
</script>
Tracking client-side route changes
The previous code snippet tracks the initial page load, which is the server-rendered app that has been delivered to the client. But once this page has been loaded and the JavaScript fully hydrated, the app switches to client-side routing and the tracking configured so far is completely blind to client-side route changes. To fix this, let’s add one more line and corresponding imports:
<script>
import { onMount } from 'svelte';
import * as Fathom from 'fathom-client';
import { page } from '$app/stores';
import { browser } from '$app/environment';
onMount(() => {
Fathom.load(import.meta.env.VITE_FATHOM_SITE_ID, {
url: 'https://firefly.maier.tech/script.js',
honorDNT: true,
});
});
// Track page view when path changes.
$: $page.url.pathname, browser && Fathom.trackPageview();
</script>
This great hack from Matt’s post took me a while to understand. This
Twitter thread gave me a crucial hint.
The last line is a Svelte
reactive statement, but it is not the
typical example from the Svelte tutorial where something is assigned
to a variable. It uses JavaScript’s
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
the app is running in a browser.
Tracking goals
Unlike tracking pageviews, there is no blueprint for tracking goals with Fathom. The actual API is
straightforward. Call
trackGoal
whenever a visitor has completed an action that you wanted them to do.
Let’s assume that 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:
<script>
export let icons;
</script>
<div>
{#each icons as icon (icon.key)}
<a href={icon.href} on:click={icon.onclick}>
<span>{icon.title}</span>
<svelte:component
this={icon.component}
/>
</a>
{/each}
</div>
Note the on:click
directive to which an (optional)
icon.onclick
callback can be assigned. When rendering social icons with the following array
const icons = [
{
key: 'twitter',
title: 'Twitter',
href: 'https://twitter.com/maiertech',
component: TwitterLogo,
onclick: () => {
Fathom.trackGoal('FATHOM_GOAL_ID', 0);
}
},
{
key: 'github',
title: 'GitHub',
href: 'https://github.com/maiertech',
component: GitHubLogo
}
];
you get a Twitter icon, which tracks goal FATHOM_GOAL_ID
when clicked. But when you click the
GitHub icon, no goal is tracked, because no callback has been assigned.