Analytics
Self-hosted, per-workspace traffic analytics joined to your CMS content.
Brandfine ships a built-in analytics surface alongside every workspace: visitor counts, top pages, sources, countries, devices, real-time view, and — the differentiating bit — a content × traffic join that links page paths back to the posts that produced them.
The backing store is a self-hosted Umami
instance on the same droplet as the CMS. From a customer's
perspective it's just "Brandfine analytics" — every report renders
inside the CMS, every URL is on the brandfine.co domain, and the
data never leaves Brandfine's infrastructure.
What you see in the CMS
Sidebar → Growth → SEO & Analytics. A tile per workspace, each in one of four states:
| State | What it means |
|---|---|
| No domain | The workspace hasn't set domain yet. Analytics needs a hostname to scope traffic to. |
| Off | Not enabled for this workspace. Click Enable (owner or admin role) to provision. |
| Waiting | Provisioned but no traffic yet. The card polls every 5s; flips to Live the moment the first pageview arrives. |
| Live | Receiving traffic. Card shows 7-day visitors/pageviews plus current active visitors. Click anywhere on the tile to drill into the full reports. |
The detail page has a range picker (24h / 7d / 30d / 90d) and seven report cards: traffic time-series, top pages, sources, countries, devices, real-time, plus Content performance and Other pages.
Content performance — the wedge
Generic analytics tools see URLs; Brandfine sees URLs + the posts
behind them. The Content performance card joins each pageview
to a workspace Post by last URL segment → Post.slug, then
surfaces:
- The post title (linked to the editor in the CMS)
- The post type (
blog,services, custom types) as a chip - The locale tag for non-default-locale variants
- View count, with a proportional bar
Pages without a matching post — homepage, section indexes, static routes, anything Brandfine doesn't recognise — land in the Other pages card instead.
The match strategy is conservative: it only looks at the last path
segment, so /blog/uk-eta-2026 matches but /products/foo/reviews
matches on reviews (probably wrong if the real post is foo).
Per-post-type URL templates are a future tightening — see the
Phase A design doc for
the trade-off note.
Deploy markers on the chart
Every CMS publish writes a PostPublished row with a publishedAt
timestamp. The traffic chart overlays those publishes as dashed
amber vertical lines so you can see traffic shifts in context. The
chart header shows a ● Publishes N pill alongside the
Pageviews / Visitors legend; hovering a bucket with publishes lists
the post titles in the tooltip.
This works retroactively — you'll see deploy markers for every
publish in PostPublished, even ones that pre-date the moment
analytics was enabled.
Privacy & data ownership
- Cookieless. Umami's tracker uses no cookies and stores no personally identifying data — no consent banner is required.
- No third-party data sharing. Events go directly to the Brandfine droplet, not to Google / Vercel / Plausible / etc.
- Brandfine is the data controller for every workspace's analytics data. One bill, one host, one privacy policy.
- Per-workspace isolation. Each workspace maps to one Umami "website"; queries are scoped at the data layer.
Two ways for your site to send pageviews
See the SDK analytics guide for the full walkthrough. The short version:
@brandfine/clientautomatically — callbf.analytics.install()once on the client and the SDK fetches this workspace's tracker config and injects the script. Use the server-component pattern in Next.js / Astro to keep the API key out of the browser bundle (and skip the runtime round-trip).- Manual
<script>tag — for sites that don't use the SDK, the CMS surfaces a paste-in<script defer src="…" data-website-id="…">on the workspace detail page under "Manual install snippet".
Once installed, the workspace card flips from Waiting to Live within ~5 seconds.
When this isn't enough
The Phase A architecture wraps Umami behind an AnalyticsProvider
abstraction (see @brandfine/analytics in the monorepo). The day
we hit a hard ceiling — multi-dimensional cohort queries, regional
data residency, ≥ 5–10M events/month per workspace — we'll swap in
a ClickHouse-backed provider behind the same interface. Customer
code and the CMS UI won't change.
Until that day comes, the simpler stack is the right stack.