akibeo / kirby-umami
Umami analytics for Kirby CMS: tracker script with CSP nonce, server-side events and a Panel stats view.
Fund package maintenance!
Requires
- php: >=8.1
- getkirby/composer-installer: ^1.2
Requires (Dev)
- getkirby/cms: ^4.0 || ^5.0
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-02 14:45:28 UTC
README
Umami analytics for Kirby. Umami is cookieless and stores no personal data, so it needs no cookie banner and no consent gating.
- Tracker script — one snippet prints the
<script>tag with every tracker option as config. - CSP nonce — picks up
cspNonce()from akibeo/kirby-csp automatically. - Off while developing — nothing is tracked while Kirby's
debugoption is on. - Server-side events —
umami()->track('contact-form')from PHP, for things that never reach the browser tracker (form endpoints, redirects). - Panel view — an "Analytics" entry with visitors, visits, pageviews, bounce rate and visit time for the last 24h / 7 / 30 / 90 days, a link to the Umami dashboard and, optionally, the embedded public share report.
- Cloud or self-hosted — works with Umami Cloud (API key) and self-hosted instances (API key or username/password).
Installation
Composer
composer require akibeo/kirby-umami
Download / Git submodule
Copy this repository into site/plugins/kirby-umami/:
git submodule add https://github.com/wdebusschere/kirby-umami.git site/plugins/kirby-umami
No build step is required — Kirby autoloads plugins from site/plugins/. The plugin registers itself as akibeo/umami and reads its options from the akibeo.umami namespace.
Configuration
Add to site/config/config.php. Only enabled and websiteId are
required; the rest are shown with their defaults.
return [ 'akibeo.umami' => [ 'enabled' => true, 'websiteId' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'src' => 'https://stats.example.com/script.js', // default: https://cloud.umami.is/script.js // Tracker options, see https://umami.is/docs/tracker-configuration 'hostUrl' => null, // origin of the Umami instance; defaults to the origin of `src` 'autoTrack' => true, // false: only events you send with umami.track() in JS 'domains' => [], // only track on these hostnames 'tag' => null, // tag every view, to filter in the dashboard 'excludeSearch' => false, // drop ?query from tracked URLs 'excludeHash' => false, // drop #hash from tracked URLs 'trackInDebug' => false, // also track while option('debug') is true 'trackPanelUsers' => true, // false: skip logged-in Panel users (see note) 'nonce' => null, // CSP nonce; defaults to cspNonce() when akibeo/kirby-csp is installed // Umami API, for the Panel view and umami()->stats() 'apiUrl' => null, // defaults to https://api.umami.is/v1 (cloud) or {hostUrl}/api 'apiKey' => null, // API key (Umami Cloud, or Settings > API keys on self-hosted) 'username' => null, // or the login of a self-hosted install 'password' => null, 'timeout' => 5, // seconds, for every request to Umami // Panel 'panel' => true, // false: hide the "Analytics" menu entry 'shareUrl' => null, // public share URL, embedded in the Panel view 'cache' => true, // cache for API tokens and stats ], ];
Keep secrets (apiKey, password) in a host config
(config.<host>.php) that is not committed.
Per-environment overrides
config.localhost.php and friends can override any key:
'akibeo.umami' => [ 'trackInDebug' => true, // test the tracker locally ],
Content-Security-Policy
With akibeo/kirby-csp the script
tag carries the per-request nonce, so script-src needs nothing extra. The
tracker posts to /api/send on the Umami host, so add that host to
connect-src:
'akibeo.csp' => [ 'directives' => [ 'connect-src' => "'self' https://stats.example.com", ], ],
Without kirby-csp, set 'nonce' => fn () => myNonce() (a callable is
resolved per request) or leave it null.
trackPanelUsers and the pages cache
When the pages cache is on, the first visitor decides what gets cached. A
logged-in Panel user hitting an uncached page would cache it without the
tracker. Leave trackPanelUsers on when the pages cache is on, or exclude
your own visits in Umami instead (localStorage.setItem('umami.disabled', 1)
in the browser console).
Usage
Tracker script
In a plain PHP template:
<?php snippet('umami') ?>
In a Blade layout:
@snippet('umami')
Or build the tag yourself:
<?= umami()->scriptTag() ?> <?= $site->umami()->scriptTag() ?>
The tag is empty when tracking is off, so it can always be printed.
Server-side events
umami()->track('contact-form', ['subject' => $subject]); umami()->track('download', ['file' => $file->filename()], $page->url());
The event joins the visitor's session in Umami: the visitor's IP and
User-Agent are forwarded. Returns false when tracking is off or Umami
could not be reached; it never throws.
Stats from PHP
$stats = umami()->stats('30d'); // 24h, 7d, 30d, 90d // ['pageviews' => 1234, 'visitors' => 567, 'visits' => 600, // 'bounces' => 210, 'totaltime' => 41000, 'prev' => [...], ...] $pages = umami()->metrics('path', '7d', 10); // ['type' => 'path', 'total' => 900, 'rows' => [['x' => '/', 'y' => 400], …]] // Types: path (url on Umami < 3), entry, exit, title, hostname, query, // referrer, channel, browser, os, device, screen, language, country, // region, city, event, tag — see Umami::METRIC_TYPES. $series = umami()->series('7d'); // ['unit' => 'day', 'timezone' => 'Europe/Brussels', // 'buckets' => [['key' => '2026-10-01', 'label' => '1 Oct', 'pageviews' => 120, 'sessions' => 80], …]] $online = umami()->active(); // visitors in the last five minutes
Results are cached for 10 minutes (one minute for active()). All of them
throw Akibeo\Umami\UmamiException when the API is unreachable or the
credentials are wrong; $e->status() holds the HTTP status. Any other
endpoint of the Umami API is reachable through umami()->api('websites/…').
Panel
"Analytics" in the Panel menu: the summary numbers with the change against
the previous period, visitors online now, a pageviews chart and the
breakdown tables of the Umami dashboard (pages, referrers, browsers,
countries, events, …) for the last 24 hours, 7, 30 or 90 days. Without API
credentials it only links to the Umami dashboard (and shows the share
report if shareUrl is set). To embed
the share report, the Umami instance has to allow framing by the Panel
origin: set ALLOWED_FRAME_URLS=https://www.example.com in the Umami
environment.
API endpoints
Both need a Panel session.
GET /api/plugin/umami/stats?range=7d returns the summary, the live count
and the chart series:
{
"status": "success",
"range": "7d",
"stats": { "pageviews": 1234, "visitors": 567, "visits": 600, "bounceRate": 35, "avgTime": "1m 8s", "avgSeconds": 68 },
"prev": { "...": "same keys for the previous period" },
"active": 12,
"series": { "unit": "day", "timezone": "Europe/Brussels", "buckets": [{ "key": "2026-10-01", "label": "1 Oct", "pageviews": 120, "sessions": 80 }] }
}
GET /api/plugin/umami/metrics?type=country&range=7d&limit=50 returns one
breakdown table:
{ "status": "success", "range": "7d", "type": "country", "total": 567, "rows": [{ "x": "BE", "y": 280 }] }
Development
composer install composer test # vendor/bin/phpunit
The logic lives in Akibeo\Umami\Umami (src/Umami.php); the test suite in
tests/ runs it against a bare Kirby app, so no Umami instance is needed.
License
MIT — © E-xperience LAB
Credits
- Wannes Debusschere