Analytics
First-party web analytics — PostHog, Google Analytics, Plausible, Mixpanel, Segment, and more — as adapters in blume.config.ts.
Blume injects analytics for you from an analytics list in blume.config.ts. Each entry is an adapter imported from blume/analytics: one per provider, plus script() for anything without one.
import { defineConfig } from "blume";
import { googleAnalytics, plausible, posthog, vercel } from "blume/analytics";
export default defineConfig({
analytics: [
posthog({ key: "phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }),
vercel(),
googleAnalytics({ id: "G-XXXXXXXXXX" }),
plausible({ domain: "docs.example.com" }),
],
});
Adapters emit into <head> in the order you list them, and you can combine as many as you like. Every adapter is plain data: what it returns is validated with the rest of your config and inlined into the built site, so it can carry any JSON value but never a function. A passthrough option that JSON can’t carry (a function, undefined, a bigint) fails validation with its path rather than disappearing from the build.
Analytics loads in production builds only. The scripts are emitted by blume build, never by blume dev, so local traffic never reaches your dashboards and you don’t need a separate “development” project.
To ask readers first, set consent: every adapter here then waits until the reader allows analytics.
Every identifier below — a project key, a measurement ID, a site token — is the public, browser-side one from the provider’s own install snippet. Each adapter renders that snippet, so it’s safe to commit to your docs repository; Blume never asks for a secret.
Adapters
| Adapter | Provider | Required |
|---|---|---|
adobe() |
Adobe Analytics | url |
amplitude() |
Amplitude | key |
clarity() |
Microsoft Clarity | id |
clearbit() |
Clearbit | key |
clics() |
Clics | projectId |
cloudflare() |
Cloudflare Web Analytics | token |
databuddy() |
Databuddy | clientId |
fathom() |
Fathom | site |
googleAnalytics() |
Google Analytics 4 | id |
googleTagManager() |
Google Tag Manager | id |
heap() |
Heap | id |
hightouch() |
Hightouch | key |
hotjar() |
Hotjar | id |
logrocket() |
LogRocket | id |
mixpanel() |
Mixpanel | token |
oneDollarStats() |
OneDollarStats | — |
pirsch() |
Pirsch | code |
plausible() |
Plausible | domain |
posthog() |
PostHog | key |
segment() |
Segment | key |
vercel() |
Vercel Web Analytics | — |
script() |
Anything else | src or content |
Blume’s docs use a client-side router, so most navigation is an in-page swap rather than a full page load. Every adapter counts those as pageviews: providers whose script follows history changes on its own need nothing extra, and for the ones that only count real loads (PostHog, Segment, Hightouch) Blume sends the pageview itself. The two exceptions are the tag managers — Google Tag Manager and Adobe load a container whose own rules decide what fires, so give it a history-change trigger.
PostHog
Pass your project API key to posthog() to add PostHog. The host defaults to PostHog Cloud US; set host for EU Cloud (https://eu.i.posthog.com) or a self-hosted instance.
analytics: [
posthog({
key: "phc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
host: "https://us.i.posthog.com", // optional, this is the default
}),
],
key and host are the two options Blume maps (host becomes api_host). Anything else you pass — persistence, capture_pageview, autocapture, disable_session_recording, and every other posthog.init option — is forwarded to posthog.init verbatim.
Blume sends a $pageview for each client-router navigation only while PostHog captures page loads alone, which is its behavior when you set neither capture_pageview nor defaults. With capture_pageview: "history_change", or a defaults date such as "2025-05-24" from PostHog’s current snippet, PostHog captures navigations itself, so Blume sends nothing extra. With capture_pageview: false, no pageviews are sent at all.
Vercel Web Analytics
Add vercel() to include Vercel Web Analytics. Blume renders Vercel’s official Astro component, which injects the first-party script served from your own domain once Web Analytics is enabled for the project in the Vercel dashboard.
analytics: [vercel()],
No keys are needed — the script reports to the project it’s deployed under. This only collects data on Vercel deployments, where the /_vercel/insights endpoint exists. Any option you pass is forwarded to the component as a prop (mode, debug, endpoint, scriptSrc, …); beforeSend is a function and can’t travel through config, so assign window.webAnalyticsBeforeSend from a script() adapter instead.
Cloudflare Web Analytics
Cloudflare Web Analytics has two setups, and only one of them needs an adapter.
Proxied zone (automatic setup). If Cloudflare serves your site — a Worker with a custom domain, Pages, or any zone with the orange cloud on — enable Web Analytics for the zone in the Cloudflare dashboard and stop there. Cloudflare injects the beacon at the edge, so leave cloudflare() out; listing it as well would count every pageview twice.
Any other host (manual setup). For a site Cloudflare doesn’t proxy, add the site under Web Analytics in the dashboard, copy the token out of the JS snippet it gives you (the token inside data-cf-beacon), and pass it here. Blume renders the same beacon tag the snippet does.
analytics: [
cloudflare({
token: "0123456789abcdef0123456789abcdef",
}),
],
The whole option object becomes the beacon’s data-cf-beacon JSON, so any other beacon setting (spa, …) passes straight through.
Google Analytics 4
Pass the measurement ID of your GA4 web data stream (G-…, under the stream’s details in the property’s Admin → Data Streams) to googleAnalytics(). Blume renders the Google tag exactly as the stream’s install instructions do: the async gtag.js loader plus the dataLayer bootstrap and config call.
analytics: [
googleAnalytics({
id: "G-XXXXXXXXXX",
anonymize_ip: true, // optional: any other `config` parameter
}),
],
id is the one option Blume maps. Anything else you pass rides on the gtag('config', …) call verbatim (send_page_view, cookie_domain, debug_mode, …). GA4’s enhanced measurement counts history changes as page views by default, so client-side navigations are tracked without any extra setup. Google Analytics takes a day or two to show new data; the Google Analytics Debugger extension shows requests as they happen.
Google Tag Manager
Pass your container ID (GTM-…, shown in the workspace header) to googleTagManager(). Blume renders the <head> half of the container snippet; the <noscript> iframe is for browsers without JavaScript, which never run analytics anyway.
analytics: [
googleTagManager({
id: "GTM-XXXXXXX",
dataLayer: "dataLayer", // optional, this is the default
}),
],
Which tags fire, and on what, is the container’s configuration. Client-side navigations reach the container as history changes, so fire pageview tags from a History Change trigger rather than the page-load one. Cookie consent banners are yours to set up in the container too.
Plausible
Pass the domain you added in Plausible to plausible(). For a self-hosted or proxied instance, set host to its origin; the default is Plausible Cloud.
analytics: [
plausible({
domain: "docs.example.com",
host: "https://plausible.example.com", // optional, defaults to https://plausible.io
}),
],
domain becomes data-domain and host picks where /js/script.js loads from. Any other option becomes its own data- attribute on the tag (api: "/api/event" → data-api). Script variants with extra behavior baked into the filename (script.outbound-links.js, …) are a different tag; load those with script().
Clics
Pass the project ID from Configure → Tracking in the Clics dashboard to clics(). Blume adds the Clics browser tracker with the required data-project-id attribute.
import { clics } from "blume/analytics";
analytics: [clics({ projectId: "YOUR_PROJECT_ID" })],
The tracker counts page loads and client-side navigations automatically, including Blume’s client router, so don’t add a second pageview hook. It also tracks outbound links by default. Set disableOutboundLinks: true to turn those events off. For a production build previewed on localhost, set allowLocalhost: true; ordinary blume dev does not load analytics.
Blume’s custom events don’t reach Clics: its browser tracker has no global event function to forward them to. To send your own events, call the track() helper from @clicsdev/tracker in your own client code.
Fathom
Pass your site ID (from the site’s script settings in Fathom) to fathom().
analytics: [
fathom({
site: "ABCDEFGH",
"honor-dnt": "true", // optional: any other `data-` setting
}),
],
site becomes data-site. Any other option becomes its own data- attribute on the tag (honor-dnt, excluded-domains, canonical, …), as strings. spa defaults to "auto" so Fathom counts the client router’s navigations; pass your own spa value to override it.
Pirsch
Pass your identification code (Settings → Developer → Identification Code in Pirsch) to pirsch().
analytics: [
pirsch({
code: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
}),
],
code becomes data-code, and Blume gives the tag the pianjs id Pirsch’s script finds itself by. Any other option becomes its own data- attribute (dev, exclude, include, domain, endpoint, …).
Databuddy
Pass your client ID (from the website’s settings in Databuddy) to databuddy().
analytics: [
databuddy({
clientId: "xxxxxxxxxxxxxxxxxxxxx",
"track-web-vitals": "true", // optional: any other `data-` setting
"skip-patterns": '["/admin/*"]',
}),
],
clientId becomes data-client-id. Any other option becomes its own data- attribute on the tag, so name it the way the attribute reads, in kebab-case (track-web-vitals, track-errors, track-outgoing-links, api-url, …). Databuddy ignores a camelCase name like trackWebVitals. Values are strings: "true" or "false" for a switch, and a JSON array for skip-patterns and mask-patterns. Databuddy reads any other list value as empty.
OneDollarStats
Add the site’s domain in OneDollarStats, then list oneDollarStats(). It needs no key, because OneDollarStats matches events to a site by the domain they come from.
analytics: [oneDollarStats()],
Each option becomes its own data- attribute on the tag, and values are strings:
hostnamereports every event under that host name (docs.example.com, with nohttps://or path) on every host, not justlocalhost, so preview deploys and staging count as that site’s traffic. Leave it unset unless every build should report under that name.devmode: "true"together withhostnamelets a localblume previewsend events. Onlocalhostthe tracker sends nothing without both, so keep them out of the committed config.urlis the collector endpoint events are posted to (https://collector.onedollarstats.com/eventsby default), not the site’s URL.autocollect: "false"turns off automatic page views."hash-routing": "true"sends a page view on every navigation, even when only the#fragmentchanges, like a table-of-contents click. Blume leaves the attribute off for"false", because the tracker turns hash routing on whenever it’s present.
Mixpanel
Pass your project token (project settings → Access Keys in Mixpanel) to mixpanel(). If the project uses EU or India data residency, set region to match, or Mixpanel drops the events.
analytics: [
mixpanel({
token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
region: "eu", // optional: "us" (default), "eu", or "in"
}),
],
token and region are the options Blume maps (region becomes api_host). Blume also sets track_pageview to "url-with-path-and-query-string", so the library tracks each navigation itself. Anything else you pass — including track_pageview — is forwarded to mixpanel.init verbatim (persistence, autocapture, record_sessions_percent, …).
Amplitude
Pass your project API key (project settings in Amplitude) to amplitude(). Blume renders the Browser SDK’s script-loader snippet: the keyed SDK bundle plus the init call.
analytics: [
amplitude({
key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
serverZone: "EU", // optional: any other `init` option
}),
],
key is the one option Blume maps. The init options start from what the dashboard snippet ships — autocapture: true and fetchRemoteConfig: true — and anything else you pass is merged over them verbatim. Autocapture covers page views and history changes, so client-side navigations count.
Segment
Pass the write key of your JavaScript source in Segment to segment(). If you serve analytics.js from a custom domain to sidestep ad blockers, set cdn to that origin.
analytics: [
segment({
key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
cdn: "https://cdn.example.com", // optional, defaults to https://cdn.segment.com
}),
],
key and cdn are the options Blume maps: the snippet loads the library from cdn and tells it to fetch its integrations from there too. Anything else you pass is forwarded as the analytics.load options verbatim (integrations, …).
Hightouch
Pass the write key of your event source in Hightouch to hightouch(). The events API host defaults to the US East region; set host (without a scheme) if your workspace uses another.
analytics: [
hightouch({
key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
host: "us-east-1.hightouch-events.com", // optional, this is the default
}),
],
key and host are the options Blume maps (host becomes apiHost). Anything else you pass is forwarded as the htevents.load options verbatim.
Heap
Pass your app ID (the environment ID under the project’s install settings in Heap) to heap().
analytics: [
heap({
id: "1234567890",
disableTextCapture: true, // optional: any other `heap.load` option
}),
],
id is the one option Blume maps. Anything else you pass becomes the config object of heap.load verbatim.
Hotjar
Pass your site ID (the number in the site’s tracking code in Hotjar) to hotjar().
analytics: [
hotjar({
id: 1234567,
version: 6, // optional, this is the default
}),
],
id is hjid and version is hjsv in Hotjar’s tracking code; the adapter takes nothing else.
Microsoft Clarity
Pass your project ID (from the project’s tracking code in Clarity) to clarity().
analytics: [
clarity({
id: "xxxxxxxxxx",
}),
],
The adapter takes nothing else; Clarity’s settings live in its dashboard.
LogRocket
Pass your app ID (org/app, from the project settings in LogRocket) to logrocket(). Blume renders the install snippet: the SDK plus the guarded LogRocket.init call.
analytics: [
logrocket({
id: "your-org/your-app",
release: "1.2.0", // optional: any other `init` option
}),
],
id is the one option Blume maps. Anything else you pass is forwarded to LogRocket.init verbatim (release, console, network, dom, …). A request or response sanitizer is a function and can’t travel through config; for those, drop the adapter and call LogRocket.init yourself from a script() adapter.
Clearbit
Pass your publishable API key (pk_…) to clearbit(). Blume renders the Clearbit tag, which loads whatever tags — Reveal, forms — you manage in Clearbit.
analytics: [
clearbit({
key: "pk_xxxxxxxxxxxxxxxx",
}),
],
The adapter takes nothing else.
Adobe Analytics
Pass the embed script URL of your Launch (Adobe Experience Platform Tags) environment to adobe(). Copy it from the environment’s install instructions under Environments in Data Collection; the production one looks like https://assets.adobedtm.com/…/launch-….min.js.
analytics: [
adobe({
url: "https://assets.adobedtm.com/xxxxxxxx/launch-xxxxxxxx.min.js",
}),
],
Blume loads the script asynchronously; what it tracks is the property’s rules. Client-side navigations don’t reload the page, so give the property a rule on history changes (or call _satellite.track from a script() adapter).
Custom scripts
Use script() to load any provider without an adapter, or a provider’s tag in a form its adapter doesn’t render. Each one renders one <script> tag and must set exactly one of src (external) or content (inline).
analytics: [
// Umami
script({
src: "https://cloud.umami.is/script.js",
strategy: "defer",
attributes: { "data-website-id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" },
}),
// An inline snippet
script({
content: "console.log('analytics ready')",
}),
],
attributes is a map of extra HTML attributes (data-*, id, …) spread onto the tag, and strategy adds async or defer to external scripts.
A root-relative src points at a file in public/ (src: "/js/redirects.js" for public/js/redirects.js), so under a deployment.base it gets the base, like a link does: with base: "/docs", the tag loads /docs/js/redirects.js. A src that already starts with the base is left as written, and an absolute or protocol-relative URL passes through, so a script served outside the site’s base takes its full URL.
Options
| Adapter | Option | Default | Description |
|---|---|---|---|
adobe() |
url |
— | Launch environment embed script URL. Required. |
amplitude() |
key |
— | Amplitude project API key. Required. |
amplitude() |
anything else | autocapture: true, fetchRemoteConfig: true |
Merged into the amplitude.init options verbatim. |
clarity() |
id |
— | Clarity project ID. Required. |
clearbit() |
key |
— | Clearbit publishable API key. Required. |
clics() |
projectId |
— | Public Clics project ID. Required. |
clics() |
allowLocalhost |
false |
Emit data-allow-localhost for local previews of a production build. |
clics() |
disableOutboundLinks |
false |
Emit data-disable-outbound-links to stop outbound-link tracking. |
cloudflare() |
token |
— | Cloudflare Web Analytics site token (manual setup). Required. |
cloudflare() |
anything else | — | Forwarded in the beacon’s data-cf-beacon JSON verbatim. |
databuddy() |
clientId |
— | Databuddy client ID. Required. |
databuddy() |
anything else | — | Rendered as a data- attribute on the tag. |
fathom() |
site |
— | Fathom site ID. Required. |
fathom() |
spa |
"auto" |
Fathom’s history-change tracking (data-spa). |
fathom() |
anything else | — | Rendered as a data- attribute on the tag. |
googleAnalytics() |
id |
— | GA4 measurement ID. Required. |
googleAnalytics() |
anything else | — | Forwarded on the gtag('config', …) call verbatim. |
googleTagManager() |
id |
— | Tag Manager container ID. Required. |
googleTagManager() |
dataLayer |
dataLayer |
Name of the data layer global the container reads. |
heap() |
id |
— | Heap app ID. Required. |
heap() |
anything else | — | Forwarded as the heap.load config verbatim. |
hightouch() |
key |
— | Hightouch event source write key. Required. |
hightouch() |
host |
us-east-1.hightouch-events.com |
Events API host, without a scheme. |
hightouch() |
anything else | — | Forwarded as the htevents.load options verbatim. |
hotjar() |
id |
— | Hotjar site ID (hjid). Required. |
hotjar() |
version |
6 |
Tracking-code version (hjsv). |
logrocket() |
id |
— | LogRocket app ID (org/app). Required. |
logrocket() |
anything else | — | Forwarded to LogRocket.init verbatim. |
mixpanel() |
token |
— | Mixpanel project token. Required. |
mixpanel() |
region |
us |
Data residency region: us, eu, or in. |
mixpanel() |
anything else | track_pageview: "url-with-path-and-query-string" |
Merged into the mixpanel.init options verbatim. |
oneDollarStats() |
hostname |
— | Bare host name every event is reported under, on every host. |
oneDollarStats() |
anything else | — | Rendered as a data- attribute on the tag ("hash-routing": "false" is left off). |
pirsch() |
code |
— | Pirsch identification code. Required. |
pirsch() |
anything else | — | Rendered as a data- attribute on the tag. |
plausible() |
domain |
— | The site’s domain in Plausible. Required. |
plausible() |
host |
https://plausible.io |
Origin of a self-hosted or proxied instance. |
plausible() |
anything else | — | Rendered as a data- attribute on the tag. |
posthog() |
key |
— | PostHog project API key. Required. |
posthog() |
host |
https://us.i.posthog.com |
PostHog ingestion host (EU Cloud or self-hosted). |
posthog() |
anything else | — | Forwarded to posthog.init verbatim. |
segment() |
key |
— | Segment source write key. Required. |
segment() |
cdn |
https://cdn.segment.com |
Origin of a custom domain proxying Segment’s CDN. |
segment() |
anything else | — | Forwarded as the analytics.load options verbatim. |
vercel() |
mode, debug, … |
— | Forwarded to the official Astro component as props. |
script() |
src |
— | External script URL. Mutually exclusive with content. |
script() |
content |
— | Inline script body. Mutually exclusive with src. |
script() |
strategy |
— | async or defer for an external script. |
script() |
attributes |
— | Extra HTML attributes spread onto the <script> tag. |
Custom events
Blume’s page feedback (feedback, and feedback_comment when written feedback is on), search (search, search_select), assistant (ask, ask_answer, ask_error), and narration player (narration_play, narration_complete) send custom events through every configured adapter that has a client API — PostHog, Mixpanel, Heap, Segment, Hightouch, Amplitude, LogRocket, Adobe, Google Analytics, Google Tag Manager (as a { event } push on window.dataLayer), Plausible, Databuddy, Fathom, OneDollarStats (with each property value as a string), Pirsch, Clarity, Hotjar, and Vercel. Fathom, Clarity, and Hotjar take an event’s name alone, so they count each event without its properties. Cloudflare and Clearbit have no event API, and Clics has no global one for Blume to call. Every event also fires as a blume:track CustomEvent on window with { event, props } in detail, so a script() adapter can forward it anywhere else. The assistant never sends a question’s text to a provider, and search sends its queries as typed unless you set search.analytics.queries to false, which sends each query’s length instead; either way the text still reaches the blume:track event, for a listener you write to forward. For deploying your built site, see Deployment.