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.
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.Integration (recommended)
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).
| Option | Type | Default | Role |
|---|---|---|---|
domain | string | location.hostname | Site identifier |
endpoint | string | https://taktlytics.com/api/event | Ingestion URL; set /api/event for a same-origin first-party proxy |
scriptOrigin | string | — | Origin to derive the endpoint from ({origin}/api/event), for a proxied custom domain; endpoint still wins |
outbound | boolean | false | Tracks outbound links |
files | boolean | false | Tracks downloads |
spa | boolean | true | Tracks client navigation (maps to core’s auto option) |
track404 | boolean | false | Reports a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status). |
tagged | boolean | false | Tracks elements carrying data-takt-event |
respectDnt | boolean | true | Respects Do Not Track |
excludeLocalhost | boolean | true | Ignores localhost / private IPs |
enabled | boolean | true | Master kill switch: set to false and no event is sent |
sampleRate | number | 1 | Fraction of visitors tracked, from 0 to 1 |
trackQuery | boolean | false | Sends the full URL instead of stripping query and hash |
queryParams | string[] | — | Whitelist of params kept when trackQuery is off |
exclude | string[] | — | Path prefixes never tracked, e.g. ['/app','/account'] (segment-bounded: /app covers /app and /app/…, not /application) |
scrubUrl | (url: string) => string | — | Integration 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-swapnever fires; - then one pageview per
astro:after-swapevent, which fires exactly once perClientRouternavigation (View Transitions DOM swaps and back/forward included), wheneverspais on.
Each navigation is therefore counted exactly once.
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.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.