Staats

Add analytics to SvelteKit

A cookieless tracker for SvelteKit, installed once in the app template and left alone.

The install

SvelteKit has one HTML file that wraps every route. Put the snippet in the <head> of src/app.html:

<!-- src/app.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    %sveltekit.head%
    <script defer data-site="YOUR_SITE_CODE" src="https://www.staats.ai/s.js"></script>
  </head>
  <body data-sveltekit-preload-data="hover">
    <div>%sveltekit.body%</div>
  </body>
</html>

Put it in app.html rather than a +layout.svelte. The app template is rendered once per document, so the tag is emitted as plain HTML and never re-evaluated. A tag placed in a root layout can re-run on client navigation, which is where duplicate pageviews come from.

Client-side navigation is already covered

SvelteKit intercepts link clicks and navigates on the client, so most page changes never trigger a document load. You do not need to subscribe to afterNavigate or watch $page.url to record them. The tracker hooks pushState, replaceState and popstate, which is what the SvelteKit router uses, so soft navigations are counted the same as hard ones.

Adding an afterNavigate hook on top will double-count every in-app navigation.

Preloading does not inflate your numbers

The default data-sveltekit-preload-data="hover" fetches a route's data when a visitor hovers a link, before any click. That is a data load, not a navigation: no history entry is pushed and no pageview is recorded. Hovering every link in your nav will not show up as traffic.

Tracking a click

Add a data-track attribute to any clickable element in a .svelte file. The tracker uses one delegated listener, so this works on elements rendered conditionally or inside an {#each} block:

<button data-track="signup_click">Start free</button>

For form submissions, or anything that is not a click, call window.track from your handler:

<script>
  function onSubmit() {
    window.track('contact_submitted')
  }
</script>

No cookie banner

The script is around 1.5KB, sets no cookies, and stores nothing on the device. Visitors are counted with a pseudonymous hash that rotates daily, and country comes from the browser timezone instead of an IP lookup. There is nothing to consent to, so no banner.

Local development

Leave the snippet in app.html while you work. localhost is never recorded, so vite dev stays out of your production numbers without an environment check.

Worth knowing: src/routes/+error.svelte renders inside the same app.html, so 404s are recorded as pageviews for paths that do not exist. If mistyped URLs and crawler probes cluttering your top pages bothers you, filter those paths when you ask, or serve your error page from a template without the snippet.

Reading the data

Staats has no dashboard. You connect it to your coding agent once, and then you ask. Because the connection is a Model Context Protocol server, the agent can pull traffic, referrers, and funnels into the conversation you are already having about the code.

> did the new pricing page help?

The agent also stores what the site is for and what each event means, so a question three weeks later does not start from nothing. See MCP Server and Agent Workflows.

Get a site code

Create a free account, add your site, and paste the snippet above. The free plan covers side projects, and there is no card required.