takt

Astro

Astro has a dedicated integration: @vskstudio/takt-astro, built on the @vskstudio/takt-core core. It injects a tiny browser runtime that boots Takt, fires the initial pageview and tracks client-side navigations — including Astro View Transitions.

pnpm add @vskstudio/takt-astro @vskstudio/takt-core

@vskstudio/takt-core (>=0.8.1) and astro (>=4) are peer dependencies. The runtime is SSR / prerender safe: it sits behind a typeof window guard and only runs in the browser.

This page is the option reference. For the end-to-end integration and its main trap — the extra astro:page-load listener that doubles every pageview — see the guide cookieless analytics on an Astro site.

Pick one of the two paths below — not both. Both boot core's default instance behind the same window.__takt flag: whichever loads second re-initialises nothing, it does strictly nothing. So you would not double-count — you would just ship a second script for no reason.

Add takt() to your Astro config integrations list:

// astro.config.mjs
import { defineConfig } from 'astro/config'
import takt from '@vskstudio/takt-astro'

export default defineConfig({
  integrations: [takt({ domain: 'example.com' })]
})

<Takt /> component

For per-layout control, place the component in a layout <head>:

---
import Takt from '@vskstudio/takt-astro/Takt.astro'
---
<head>
  <Takt domain="example.com" />
</head>

The integration and the component accept the same options, with one exception: scrubUrl is a function and is accepted only by the integration (see the note under the table).

OptionTypeDefaultRole
domainstringlocation.hostnameSite identifier
endpointstringhttps://taktlytics.com/api/eventIngestion URL; set /api/event for a same-origin first-party proxy
scriptOriginstringOrigin to derive the endpoint from ({origin}/api/event), for a proxied custom domain; endpoint still wins
outboundbooleanfalseTracks outbound links
filesbooleanfalseTracks downloads
spabooleantrueTracks client navigation (maps to core’s auto option)
track404booleanfalseReports a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status).
taggedbooleanfalseTracks elements carrying data-takt-event
respectDntbooleantrueRespects Do Not Track
excludeLocalhostbooleantrueIgnores localhost / private IPs
enabledbooleantrueMaster kill switch: set to false and no event is sent
sampleRatenumber1Fraction of visitors tracked, from 0 to 1
trackQuerybooleanfalseSends the full URL instead of stripping query and hash
queryParamsstring[]Whitelist of params kept when trackQuery is off
excludestring[]Path prefixes never tracked, e.g. ['/app','/account'] (segment-bounded: /app covers /app and /app/…, not /application)
scrubUrl(url: string) => stringIntegration only. Rewrites each URL before it is sent
scrubUrl is a function: it is serialised at build time via toString() then re-evaluated in the browser. It must therefore be self-contained — no closure variables, no outer-scope references — and never built from user input. The <Takt /> component serialises its config as a JSON data island, which cannot carry a function: passing scrubUrl to it throws at build time rather than silently doing nothing.

View Transitions

Astro’s ClientRouter runs several history operations per navigation (a scroll-restoration replaceState, then a pushState), so core’s history patch would over-count. The integration rules it out: it boots core with auto: false, which disables both the pushState/replaceState patch and the boot pageview.

The runtime drives pageviews from Astro’s own lifecycle instead:

  • one explicit initial pageview when the script boots — which also covers plain MPA sites, where the script re-runs on every page load and astro:after-swap never fires;
  • then one pageview per astro:after-swap event, which fires exactly once per ClientRouter navigation (View Transitions DOM swaps and back/forward included), whenever spa is on.

Each navigation is therefore counted exactly once.

Do not add an astro:page-load listener to track pages: the runtime already registers its own astro:after-swap. Yours would install a second one and count every navigation twice.
Corollary: because core's history patch is off, a history.pushState you drive by hand — filters, tabs, pagination outside the ClientRouter — fires no astro:after-swap, hence no pageview. That is the one case where calling pageview() yourself is the right answer.

Custom events

The core functions are re-exported for convenience:

import { track } from '@vskstudio/takt-astro'

track('Signup', { revenue: { amount: '9.00', currency: 'USD' } })

track/pageview/optOut/optIn run in the browser — call them from a client <script> or a hydrated UI-framework component.