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.10.0 since 0.8.0) 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 | none | 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[] | none | Whitelist of params kept when trackQuery is off |
exclude | string[] | none | Path prefixes never tracked, e.g. ['/app','/account'] (segment-bounded: /app covers /app and /app/…, not /application) |
scrubUrl | (url: string) => string | none | Integration only. Rewrites each URL before it is sent (page, referrer, url of outbound links and downloads, path of 404s) |
debug | boolean | false | Logs each payload to the browser console before sending |
redactRoutes | string[] | none | Route patterns sent in place of the real path, e.g. ['/verify/[token]'] sends /verify/[token] instead of /verify/abc123 (see Route redaction) |
redactUndecodablePaths | boolean | true | Not exposed by this wrapper: set it on the core SDK (init() / createTakt()), see Unreadable paths |
routeTemplates | boolean | false | Sends every page as its Astro route template (/blog/[slug]), read from Astro.routePattern. Needs Astro 5 and the route meta tag |
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.Route redaction
The query string is stripped by default, but the path is sent as is, so /verify/abc123 leaks the token. List the sensitive routes in redactRoutes and every matching path is sent as its pattern, while the other pages keep their real path:
takt({ redactRoutes: ['/verify/[token]', '/invoices/[id].pdf'] }) The rule covers the page URL, same-site referrers, outbound and download link destinations, and 404 paths. Patterns use Astro’s syntax ([param], [...rest]) and also accept :param and *.
To send every page as its route template instead (/blog/hello becomes /blog/[slug]), turn on routeTemplates. Astro knows the template on the server (Astro.routePattern), so it travels in a <meta name="takt:route"> tag that the runtime reads at each pageview, including after a ClientRouter navigation. With the integration, render that tag with <TaktRoute /> in the <head> of your layout:
// astro.config.mjs
export default defineConfig({
integrations: [takt({ routeTemplates: true })]
}) ---
import TaktRoute from '@vskstudio/takt-astro/TaktRoute.astro'
---
<head>
<TaktRoute />
</head> The <Takt /> component renders the tag itself when routeTemplates is set:
<Takt domain="example.com" routeTemplates /> Astro.routePattern exists from Astro 5. On Astro 4 no tag is rendered, so redactRoutes still applies and the real path is sent otherwise. This mode merges every article of a public blog into one row, so it suits private areas better; redactRoutes is usually the right tool for a public site. Details in Privacy.
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/isOptedOut run in the browser: call them from a client <script> or a hydrated UI-framework component.
Consent
optOut, optIn and isOptedOut do not wait for the runtime to boot: they read and write the stored choice directly. A consent banner can therefore run before the injected runtime, and the instance it boots later honours the choice. Call them from a client <script>, since the choice lives in the browser.
<button id="analytics-toggle"></button>
<script>
import { isOptedOut, optIn, optOut } from '@vskstudio/takt-astro'
const button = document.getElementById('analytics-toggle')
const render = () => {
if (button) button.textContent = isOptedOut() ? 'Enable analytics' : 'Disable analytics'
}
button?.addEventListener('click', () => {
if (isOptedOut()) optIn()
else optOut()
render()
})
render()
</script>