takt

Solid

SolidJS has a dedicated wrapper: @vskstudio/takt-solid (component, accessor, declarative click, web component), built on the @vskstudio/takt-core core.

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

solid-js (^1.8) and @vskstudio/takt-core (>=0.8.1) are peer dependencies. It is a thin SSR-safe layer (init only happens on mount, in the browser), fully typed, that never changes the wire payload or the core’s privacy guarantees. It exposes two entry points:

  • @vskstudio/takt-solid — the <Takt> component, the useTakt() accessor, createTaktEvent(), the <TaktEvent> component, the <TaktBadge> / <TaktEmbed> widgets, plus the core helpers it re-exports (badgeUrl(), embedUrl(), createStats(), PublicApiError).
  • @vskstudio/takt-solid/element — the Solid-free <takt-analytics> web component for plain HTML.

<Takt> component + useTakt()

Place <Takt> once near the root: it boots analytics in onMount, emits the initial pageview, wires up SPA navigation and publishes the instance into a module store. useTakt() then retrieves it from anywhere in the app — as long as you call the accessor after that mount.

import type { JSX } from 'solid-js'
import { Takt } from '@vskstudio/takt-solid'

export function App(props: { children: JSX.Element }) {
  return (
    <Takt domain="example.com" outbound files={['pdf', 'zip']}>
      {props.children}
    </Takt>
  )
}

Call useTakt() inside the handler, not in the component body: a descendant’s body runs before <Takt>’s onMount, so an instance captured there would stay the no-op forever and every event would go nowhere. A click handler, a createEffect or createTaktEvent() all resolve the instance after mount.

import { useTakt } from '@vskstudio/takt-solid'

export function SignupButton() {
  return (
    <button
      onClick={() =>
        useTakt().track('Signup', {
          props: { plan: 'pro' },
          revenue: { amount: '29.00', currency: 'EUR' }
        })
      }
    >
      Sign up
    </button>
  )
}

<Takt> component props:

PropTypeDefaultRole
domainstringlocation.hostnameSite identifier
endpointstringhttps://taktlytics.com/api/eventIngestion URL. Pass /api/event for a same-origin first-party proxy.
scriptOriginstringFirst-party origin to derive the endpoint from (that origin followed by /api/event). endpoint wins over it.
outboundbooleanfalseTracks outbound links
filesboolean \| string[]falseTracks downloads (optional extension list)
spabooleantrueTracks client navigation (auto pageviews)
track404booleanfalseReports a 404 event on error pages ([data-takt-404] / <meta name="takt:404"> marker, or a 404 HTTP status).
taggedbooleanfalseAuto-tracks clicks on [data-takt-event] elements
respectDntbooleantrueRespects Do Not Track
excludeLocalhostbooleantrueIgnores localhost / private IPs
enabledbooleantrueMaster switch — false disables all tracking
sampleRatenumber1Fraction of sessions tracked (0–1)
trackQuerybooleanfalseKeeps the query string in URLs
queryParamsstring[]Parameters kept when trackQuery is false (allowlist)
excludestring[]Path prefixes never tracked. Segment-bounded: /app covers /app and /app/… but not /application.
scrubUrl(url: string) => stringTransforms every URL before it is sent

Config props are read once when <Takt> mounts — changing them afterwards has no effect; remount the component to reconfigure.

The component forwards all of this configuration to the core: the advanced settings (enabled, sampleRate, trackQuery, queryParams, exclude, scrubUrl) are available as props, with no separate core instance. Only debug is not exposed: to enable it, instantiate the core yourself with createTakt() — and do not use <Takt> at the same time, or you will double your pageviews.

useTakt() never throws: called outside a <Takt>, during SSR, or before <Takt> has mounted, it returns a no-op instance that sends nothing over the network and warns once in the console (useTakt() called before <Takt> mounted). That message is the symptom of the pitfall above.

createTaktEvent() + <TaktEvent>

For declarative click tracking, two ways that resolve the active instance at click time (no stale closure), with a fallback to the core’s default instance.

createTaktEvent() returns an { onClick } you spread onto any element:

import { createTaktEvent } from '@vskstudio/takt-solid'

export function BuyButton() {
  const onBuy = createTaktEvent({
    name: 'Purchase',
    revenue: { amount: '29.00', currency: 'EUR' }
  })
  return <button {...onBuy}>Buy</button>
}

<TaktEvent> wraps a single child and composes its existing onClick, so you can annotate an element without touching its handler:

import { TaktEvent } from '@vskstudio/takt-solid'

export function SignupCta(props: { onClick: () => void }) {
  return (
    <TaktEvent name="Signup" props={{ plan: 'pro' }}>
      <button onClick={props.onClick}>Sign up</button>
    </TaktEvent>
  )
}

The child’s original onClick still fires; tracking runs alongside it. The child must render an actual DOM element: otherwise tracking is disabled, with a console warning.

<TaktBadge> and <TaktEmbed> widgets

To display a site’s public stats (see Widgets), the package ships two drop-in components:

import { TaktBadge, TaktEmbed } from '@vskstudio/takt-solid'

export function Footer() {
  return (
    <>
      <TaktBadge domain="example.com" variant="b" glyph="dash" lang="en" />
      <TaktEmbed domain="example.com" theme="dark" lang="en" />
    </>
  )
}

<TaktBadge> renders an <img> (loading="lazy", decoding="async"): variant is a, b or d, glyph is unplug, dash, off or eyeoff. <TaktEmbed> renders a 404×264 <iframe> by default, loading="lazy" and sandboxed: theme is light, dark or auto. Both accept lang (fr / en), a host prop to point at another Takt origin, and forward their remaining attributes to the rendered element. The re-exported createStats() client serves the same public stats as JSON.

<takt-analytics> web component

For plain HTML or a non-Solid framework, import the ./element subpath (auto-registered, bundles the core, no Solid runtime):

import '@vskstudio/takt-solid/element'
<takt-analytics domain="example.com" outbound files></takt-analytics>

Attributes mirror the props, in kebab-case:

  • Flags (presence = on, value ignored): outbound, files, track-404, tagged.
  • On by default, set ="false" (or ="0") to turn them off: spa, respect-dnt, exclude-localhost.
  • Values: domain, endpoint, script-origin, sample-rate, plus query-params and exclude which take a comma-separated list. enabled and track-query are only read when the attribute is present, and are turned off with ="false" / ="0".
<takt-analytics domain="example.com" spa="false" respect-dnt="false"></takt-analytics>

files takes no extension list here, and scrubUrl has no equivalent: those are values only the <Takt> component receives as props.

<Takt> boots inside onMount and is guarded by Solid's isServer: nothing touches window/document on the server, and useTakt() returns the no-op during the SSR pass. Importing ./element on the server is a no-op while customElements does not exist.