npm.io
0.3.2 • Published 9h ago

@byteflavour/goatcounter-stats-client

Licence
MIT
Version
0.3.2
Deps
0
Size
25 kB
Vulns
0
Weekly
0

@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.