Install guide
Add website analytics to SvelteKit
src/app.html is the simplest place. The root layout works too.
The tag goes in src/app.html, inside <head>. That file wraps every page SvelteKit renders, server-side and client-side both.
<script defer src="https://app.feasible.lol/js/fs-k7m2q4x5r3n6t2v5.js"></script>Steps
- Open
src/app.html. - Paste the tag inside
<head>, next to%sveltekit.head%. - Deploy.
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script defer
src="https://app.feasible.lol/js/fs-k7m2q4x5r3n6t2v5.js"></script>
%sveltekit.head%
</head>
Or the root layout
If your project keeps head material in Svelte rather than in app.html, use <svelte:head> in src/routes/+layout.svelte:
<svelte:head>
<script defer
src="https://app.feasible.lol/js/fs-k7m2q4x5r3n6t2v5.js"></script>
</svelte:head>
Both work. app.html is simpler to reason about, because there’s one of it.
The nested-layout trap
SvelteKit lets you have a +layout.svelte at any depth. Put the tag in src/routes/(marketing)/+layout.svelte and you track the marketing pages and nothing else - the app section reports no visitors at all, which reads as a traffic problem rather than a tracking one.
It belongs in the root: src/routes/+layout.svelte, or src/app.html. Never in both, or every visit counts twice.
Navigation is already handled
SvelteKit’s client-side router uses pushState, and the script listens for pushState, replaceState, popstate and hashchange, counting one pageview each. Consecutive changes inside a tick collapse to one event at the final URL, so a redirect() in a load function doesn’t produce two.
So there’s nothing to subscribe to. No afterNavigate, no $page store effect, no navigating watcher. If you add one on top, every number doubles.
Prerendered pages are fine
export const prerender = true bakes the tag into the static HTML like everything else in app.html. Static adapters, edge adapters, Node - same tag, no difference.Custom events
From anywhere that runs in the browser:
window.feasible("Signup", { props: { plan: "annual" } });
Guard it with if (browser) from $app/environment if it might run during SSR.
Check it worked
Deploy, or run vite build and vite preview on a real hostname. localhost is not counted, so npm run dev shows nothing however correct the tag is.
Open the site, click a link to a second route rather than reloading, and watch the network tab: one POST to /api/event per route. Then open Feasible.
You should show up under Real-time visitors. If you don’t, go to Site settings → Ingestion health. It counts every event that arrived and every one that was dropped, each with a named reason - unknown_site means the snippet is for a site you haven’t registered, hostname_not_allowed means the page is on a hostname that isn’t on your list, which is what a preview deployment URL will do.
The send a test event button there posts through the real public URL, so it exercises what a browser does.
A brand-new site takes about fifteen seconds before its first event is accepted, so wait for that before assuming the worst.
Related: Astro, React, the install docs, and script options.