@byteflavour/goatcounter-stats-client
@byteflavour/goatcounter-stats-client
Typed, zero-dependency client — plus a drop-in <goatcounter-stats> web
component — for goatcounter-stats-gateway,
a small public gateway that serves cached, aggregate pageview stats from a
private GoatCounter instance.
The package version always equals the gateway version it was released with, and its types are build-gated against the gateway's committed OpenAPI contract: client and server cannot drift apart.
Install
npm add @byteflavour/goatcounter-stats-client
Or without any toolchain, straight from a CDN:
<script type="module"
src="https://esm.sh/@byteflavour/goatcounter-stats-client/element"></script>
<goatcounter-stats gateway="https://stats.example.com" period="7d"></goatcounter-stats>
Headless client
Works anywhere fetch exists — browsers, node, bun, deno, SSR. Expected
failures are values, never exceptions:
import { fetchSummary, fetchPages } from "@byteflavour/goatcounter-stats-client";
const summary = await fetchSummary("https://stats.example.com", { period: "7d" });
if (summary.ok) {
summary.data.total; // number
summary.data.days; // [{ day: "2026-08-21", count: 12 }, ...] gap-free
summary.data.generated_at; // when the data was actually fetched upstream
} else {
summary.code; // "invalid_period" | "upstream_unavailable" | "network" | "invalid_response"
}
const pages = await fetchPages("https://stats.example.com"); // top 20, count desc
Deployments choose which stats panels they expose (referrers, browsers, systems, sizes, locations, languages, campaigns — all aggregate). The policy is discoverable, and every panel shares one shape:
import { fetchPanels, fetchPanel } from "@byteflavour/goatcounter-stats-client";
const policy = await fetchPanels(base); // { panels: ["summary", ...] }
const refs = await fetchPanel(base, "referrers"); // { items: [{ name, count }] }
// panel disabled by the deployment -> { ok: false, code: "panel_disabled" }
generated_at is the honesty field: the gateway serves cached — and
during backend outages, stale — data by design; this tells your UI how
fresh the numbers are.
<goatcounter-stats> element
import "@byteflavour/goatcounter-stats-client/element";
<goatcounter-stats gateway="https://stats.example.com" period="30d"></goatcounter-stats>
| Attribute | Meaning |
|---|---|
gateway |
Base URL of the gateway (required) |
period |
7d or 30d (default 30d) |
panels |
Optional restriction (e.g. summary,referrers), intersected with the deployment's policy |
State: data-state="loading" | "ready" | "error" on the host element.
The element is self-configuring: it discovers the deployment's enabled panels and renders them all (empty ones skipped) — server-side policy changes reshape every embedding page automatically.
Styling
Shadow DOM inside; the public interface is CSS custom properties and parts:
goatcounter-stats {
--gcs-accent: #e91e63; /* total figure + default bar color */
--gcs-bar: #7b1fa2; /* chart bars */
--gcs-text: inherit;
--gcs-muted: #999; /* captions, per-page counts */
--gcs-gap: 2px;
--gcs-chart-height: 4rem;
}
goatcounter-stats::part(page) { border-bottom: 1px solid #eee; }
goatcounter-stats[data-state="error"] { opacity: 0.6; }
Parts: total, chart, bar, pages, page, panel, panel-title, error.
It's a standard custom element — plain HTML, Vue, React, Angular, no
wrappers needed. (Vue/Nuxt: mark the tag as a custom element in
compilerOptions.isCustomElement.)
License
MIT. Part of the goatcounter-stats-gateway project — full documentation ships with each gateway release.