A modern, beautiful, open-source alternative dashboard for GoatCounter analytics. Dark mode by default, interactive charts, fully responsive β and zero servers, zero build step, zero cost. It's a single index.html file that you can host anywhere (GitHub Pages, Netlify, your local disk).
- π Interactive charts β area chart for traffic, donut charts for browsers/OS/devices, horizontal bars for countries and languages.
- πΊ Choropleth world map β countries shaded by visitor count (square-root scale so small markets stay visible), with hover tooltips and a gradient legend.
- π¬ Demo mode β one click on the connect screen loads the full dashboard with realistic sample data. No API key, no account, no setup β useful for trying before connecting and for sharing on social/forums.
- π Dark & light mode β defaults to your system preference, toggle persisted across sessions.
- π Flexible date ranges β Today, 7d, 30d, 90d, or a custom range.
- π Period-over-period trends β every KPI card compares against the previous equivalent window.
- π Drill-down everywhere β click any page for its referrers, any browser/OS/device for version details, any country for region breakdown, or any campaign for referrer URLs. World map highlights the selected country.
- β‘ Smart loading β KPIs and the traffic chart load first; donut, geo, and campaign breakdowns lazy-fetch only when you scroll to them.
- πΎ 60-second response cache β switching themes, toggling date ranges, or revisiting a tab within a minute makes zero network requests.
- π Per-card retry β if a card fails (rate limit, hiccup), recover it with a single click instead of refreshing the whole dashboard.
- π¦ Rate-limit aware β strictly sequential request queue with a 500ms gap; never bursts the GoatCounter API.
- π Loading indicator β "Loading X of Yβ¦" while fetching, "Updated Xs ago" when idle.
- π± Fully responsive β single column on mobile, two-column on tablet, full grid on desktop.
- π Zero build step β single
index.html, React + Recharts loaded from CDN. Open in a browser and it works. - π Privacy-first β your API key is stored only in your browser's localStorage. Nothing is sent anywhere except the GoatCounter API directly.
- βΏ Accessible β proper ARIA roles, keyboard navigation, focus indicators, and skeleton loading states.
- Visit abhishekhsingh.github.io/goatcounter-dashboard.
- Enter your GoatCounter URL (e.g.
https://yoursite.goatcounter.com) and an API key β or click Try Demo to explore with realistic sample data, no account needed. - That's it β your dashboard is live.
You don't need to trust someone else's hosting. Run your own copy in two minutes:
- Fork this repo (or just download
index.html). - Enable GitHub Pages for the repo (Settings β Pages β Source:
mainbranch, root). - Visit
https://<your-username>.github.io/goatcounter-dashboard. Done.
Or just drop index.html into any static host (Netlify, Cloudflare Pages, S3, your own server) β there's no build step.
git clone https://github.com/abhishekhsingh/goatcounter-dashboard.git
cd goatcounter-dashboard
# Open directly:
open index.html
# Or serve it with anything that serves static files:
python3 -m http.server 8000
# β visit http://localhost:8000- Log into your GoatCounter site.
- Click your username (top right) β Settings β API tab.
- Click Create key.
- Grant the Count and Read statistics permissions.
- Copy the key and paste it into the connect screen.
This dashboard talks to GoatCounter's public REST API (/api/v0). Requests are strictly serialized client-side with a 500ms gap between completions so the wire rate (including unavoidable CORS preflights) stays around 2 req/sec β comfortably under GoatCounter's bucket regardless of its exact implementation.
| Endpoint | Purpose | Tier |
|---|---|---|
GET /api/v0/me |
Verify connection on the connect screen | connect |
GET /api/v0/stats/total |
Total-visitors KPI + previous-period total for trend | 1 (immediate) |
GET /api/v0/stats/hits |
Traffic time-series + top pages list | 1 (immediate) |
GET /api/v0/stats/hits/{path_id} |
Per-page referrer drill-down (on click) | on-demand |
GET /api/v0/stats/browsers |
Browser donut chart | 2 (lazy) |
GET /api/v0/stats/browsers/{id} |
Browser version drill-down (on click) | on-demand |
GET /api/v0/stats/systems |
OS donut chart | 2 (lazy) |
GET /api/v0/stats/systems/{id} |
OS version drill-down (on click) | on-demand |
GET /api/v0/stats/sizes |
Device breakdown | 2 (lazy) |
GET /api/v0/stats/sizes/{id} |
Device size drill-down (on click) | on-demand |
GET /api/v0/stats/locations |
Countries chart | 3 (lazy) |
GET /api/v0/stats/locations/{id} |
Country region drill-down (on click) | on-demand |
GET /api/v0/stats/languages |
Languages chart | 3 (lazy) |
GET /api/v0/stats/campaigns |
Campaigns table (when present) | 4 (lazy) |
GET /api/v0/stats/campaigns/{id} |
Campaign referrer drill-down (on click) | on-demand |
Tiering: Tier 1 fires on initial load. Tiers 2/3/4 lazy-fetch via IntersectionObserver when their section enters the viewport, so the initial network burst is just 3 requests regardless of how many breakdowns the dashboard renders.
Caching: every successful response is cached in localStorage for 60 seconds keyed by (baseURL, endpoint, params). Refresh in the settings menu clears the cache and forces a fresh fetch; Disconnect wipes both the cache and stored credentials.
Demo mode bypasses all of the above. The dashboard renders from in-memory sample fixtures β zero requests, zero localStorage writes, zero rate-limit interaction.
Five scripts loaded from unpkg, in the order the page needs them:
- React 18 + ReactDOM 18 (UMD)
- prop-types 15 (UMD) β required at runtime by Recharts' UMD build. Recharts treats
PropTypesas an external global; without this script tag the chart layer crashes during init. - Recharts 2 β area chart for traffic, donut charts for browsers/OS/devices, horizontal bar lists for countries and languages
- Babel Standalone β in-browser JSX compilation
Plus:
- Inter + JetBrains Mono from Google Fonts
- Plain CSS with custom properties for theming β no Tailwind, no preprocessor, no bundler
- One generated static asset:
assets/world-map.jsβ country path data for the choropleth, regenerated only when the underlying dataset changes viascripts/build-world-map.js
The dashboard itself is a single index.html. No runtime build step.
Working today:
- β Connect / disconnect with persisted credentials
- β Demo mode with realistic sample data (no account needed)
- β All 9 GoatCounter stat endpoints integrated
- β Period-over-period trend on the visitors KPI
- β Drill-down on every card β pages (referrers), browsers/OS/devices (versions), countries (regions), campaigns (referrer URLs)
- β Choropleth world map with hover tooltips (sqrt color scale)
- β Dark / light theme with system-preference default
- β Today / 7d / 30d / 90d / Custom date ranges
- β Strict-sequential rate-limited request queue
- β
60-second
localStorageresponse cache - β
Lazy-loaded breakdowns via
IntersectionObserver - β Per-card retry button on failed endpoints
- β "Loading X of Yβ¦" / "Updated Xs ago" freshness indicator
- β Skeleton loading states, fault-tolerant error handling
Ideas for the future: CSV export, multi-site switcher, real-time mode, stale-while-revalidate cache.
PRs welcome! For most changes the flow is:
- Fork & clone.
- Edit
index.html. - Open it in a browser to test.
- Open a PR.
The only piece of the repo that needs Node is scripts/, which regenerates assets/world-map.js from pinned source data. You only run it when the country dataset itself needs updating β see scripts/README.md for the recipe. Touching the dashboard never requires it.
These keep the dashboard simple and trustworthy. PRs that violate them are unlikely to be merged:
- No runtime build step. The dashboard runs in the browser as-is. The single static asset that's "built" β
assets/world-map.jsβ is regenerated only when the underlying country dataset needs updating, viascripts/build-world-map.js. Routine code changes don't need npm. The dashboard itself never sees Node. - No new runtime dependencies beyond the five already loaded from a CDN (React, ReactDOM, prop-types, Recharts, Babel-standalone).
prop-typeslooks unused if you grep the source, but Recharts' UMD build calls it during module init β removing the script tag silently breaks all charts. Adding another script tag needs a strong reason and an issue to discuss it first. - No analytics, telemetry, or tracking. This is a privacy-friendly dashboard for a privacy-friendly analytics tool β it cannot phone home. The only network calls allowed are to the user's GoatCounter instance.
- No backend. No server-side proxy, no serverless function, no edge worker. Everything must run in the browser against the GoatCounter API directly.
- Credentials stay local. API keys live in
localStorageand only travel to the user's GoatCounter site over HTTPS. Never log them, never send them anywhere else. - Keep it deployable to GitHub Pages. Anything that breaks plain static hosting (relative paths, framework conventions, etc.) is out of scope.
- Match the existing style. Plain CSS with custom properties, no Tailwind / styled-components / CSS-in-JS. React function components with hooks. No class components.
Bug reports and feature ideas β GitHub Issues.
Having issues connecting? See the Troubleshooting Guide.
MIT β see LICENSE.
Abhishekh Singh
- Portfolio: abhishekhsingh.github.io
- GitHub: @abhishekhsingh
GoatCounter itself is built by Martin Tournoij and is wonderful β go support the project if you find it useful.
