rllngr / kirby-moniter
Kirby CMS plugin — exposes a protected status endpoint for the Moniter dashboard
Requires
- php: >=8.0
- getkirby/composer-installer: ^1.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A lightweight Kirby CMS plugin that connects client websites to Moniter, a personal dashboard to look after client sites: uptime, Kirby / plugin versions, content activity, cookieless audience and monthly reports.
The plugin exposes three secured endpoints (/moniter/status, /moniter/stats, /moniter/seo), counts page views without cookies, and adds an Audience view to the Panel that works on its own, without Moniter.
What Moniter does with it
- Uptime & response time — periodic HTTP checks (15 min → 12 h), push + email alert on offline / online transitions, and a slow response alert when the last 3 checks are above 3× the weekly median (and at least 1.5 s).
- Versions — Kirby, PHP and every installed plugin, compared to the latest stable releases on Packagist. Updates made outside Moniter (GitHub Action, FTP) are picked up from version bumps between checks.
- Remote updates — over SSH: backup of
composer.*,vendor/,kirby/,site/plugins/,composer updateof the outdated packages, then a check of the site and of/moniter/status, with automatic rollback if the site stops answering. - Content activity — last modified pages and page count, used to spot clients who publish and sites untouched for 90 days.
- Audience — the counters from
/moniter/stats, synced every hour: key figures compared to the previous period, entry pages, sources, country map, devices, languages, clicks, campaigns. - Activity feed — what's moving across all clients over 7 or 30 days: audience peaks and trends, content, health, things to handle (updates, contracts, backups, draft reports).
- Monthly reports — sent by email to the client, built from the same figures.
Installation
Via Composer (recommended)
composer require rllngr/kirby-moniter
Manual
Copy the folder into site/plugins/moniter/.
Configuration
Add the API key to site/config/config.php:
return [ 'moniter.key' => 'your-key-generated-by-moniter', // ... ];
The key is automatically generated by the Moniter dashboard when adding a client.
Endpoint
GET /moniter/status
Header: X-Moniter-Key: <key>
Response
{
"kirby": "5.3.2",
"php": "8.3.6",
"php_package": "8.3.6-0ubuntu0.24.04.13",
"php_sapi": "fpm-fcgi",
"stats": true,
"plugins": {
"author/plugin": "1.2.0"
},
"content": {
"last_modified": "2026-10-15T10:30:00",
"pages_count": 42,
"latest": [
{ "title": "Projets", "uri": "projets", "url": "https://www.example.com/projets",
"modified": "2026-10-15T10:30:00", "template": "projects" }
]
}
}
php_package is the version of the Debian / Ubuntu package that runs PHP, read from /var/lib/dpkg/status: these distributions ship security fixes without changing PHP_VERSION, so 8.3.6 alone cannot tell a patched server from an unpatched one. It is null on other systems, or when the host does not let PHP read that file (open_basedir). php_sapi is how PHP is run (fpm-fcgi, apache2handler, cgi-fcgi, litespeed…).
stats tells Moniter whether page views are counted (option on and pdo_sqlite available). content lists the published pages (drafts excluded), most recently modified first:
return [ 'moniter.limit' => 10, // number of pages in `latest` 'moniter.collections' => ['projets', 'blog'], // only count pages under these top-level sections ];
Error (invalid key)
{ "error": "Unauthorized" }
HTTP 401
Page views (cookieless)
Since v1.2.0 the plugin counts page views without cookies or identifiers. Stored per day: page path + view count, and the referring domain for external visits. No IP, no User-Agent, no cookie — so no consent banner is needed.
Since v1.3.0 it also stores the reading time per page, the browser's time zone (Europe/Paris) and language (fr). Since v1.4.0:
- Reading time only counts while the tab is visible and the visitor interacted (scroll, pointer, keyboard, touch) in the last 30 s — an idle open tab stops counting. Capped at 30 min per view.
- Country: time zone + region of the browser languages, stored together. The region is the first one found in the whole
navigator.languageslist (['fr', 'fr-BE', 'en']→ BE), not only in the first language. Moniter uses the time zone first; the region only decides between the countries sharing that zone, or when the zone is unknown (UTCin privacy browsers). The IP address is never read. - Device:
mobile/tablet/desktop, decided in the browser from the primary pointer (touch or not) and the screen size — more accurate than the User-Agent (iPads present themselves as Macs). - Entries & bounce: a view is an entry when it is the first load of the tab and comes from another site or from nowhere (reloads excluded). The entry path is kept in
sessionStoragefor the life of the tab; if a second page is viewed, the entry is marked as continued. Bounce = entries not continued. Entries without referrer = direct.utm_campaign(orutm_source) of entries = campaigns. - Actions: clicks on
tel:(number),mailto:(address), links to another site (domain only) and files (.pdf,.docx,.zip… file name only).
Each counter lives in its own table, per day: they are never combined into a per-visitor record. sessionStorage is the only browser storage, it holds no identifier and is wiped when the tab closes.
These are page views, not visitors: without an identifier, unique visitors cannot be counted.
- Kirby sites: nothing to do. A
<script src="/moniter/beacon.js" defer>is added to every HTML page (works with the page cache and with ascript-src 'self'CSP). - Filtered at count time: bots / monitoring tools (by User-Agent, never stored), logged-in Panel users, foreign origins, Panel / API / media paths, query strings.
- Storage: SQLite in
site/logs/moniter/stats.sqlite(needspdo_sqlite), 400 days retention.
return [ 'moniter.stats' => true, // false to disable 'moniter.stats.origins' => ['https://www.example.com'], // headless fronts allowed to send hits ];
Headless / Nuxt
Since v1.4.0 the beacon follows client-side navigation (history.pushState), so a front only needs the script, loaded from the CMS. Add the front's URL to moniter.stats.origins, then in nuxt.config.ts:
export default defineNuxtConfig({ app: { head: { // e.g. https://cms.example.com/moniter/beacon.js script: [{ src: process.env.NUXT_PUBLIC_MONITER_BEACON, defer: true }], }, }, })
If the front has a CSP, allow the CMS domain in script-src and connect-src. The moniter.client.ts plugin from earlier versions keeps working (page views, time, country, language) but sends no device, entries or actions: replace it with the script.
Export
GET /moniter/seo (same header) returns the SEO score computed by kirby-visibilizer, or a 501 when that plugin isn't installed. Each call also checks up to 25 external links not checked yet.
GET /moniter/stats?since=YYYY-MM-DD
Header: X-Moniter-Key: <key>
{ "since": "2026-09-01", "today": "2026-10-02", "version": 2,
"pages": [{ "d": "2026-10-02", "p": "/projets", "v": 12, "s": 540 }],
"referrers": [{ "d": "2026-10-02", "h": "google.com", "v": 3 }],
"zones": [{ "d": "2026-10-02", "z": "Europe/Paris", "r": "FR", "v": 11 }],
"langs": [{ "d": "2026-10-02", "l": "fr", "v": 10 }],
"devices": [{ "d": "2026-10-02", "t": "mobile", "v": 8 }],
"entries": [{ "d": "2026-10-02", "p": "/", "v": 6, "c": 2, "x": 3 }],
"actions": [{ "d": "2026-10-02", "t": "tel", "g": "+33233000000", "v": 1 }],
"campaigns": [{ "d": "2026-10-02", "c": "newsletter-octobre", "v": 2 }] }
entries: v arrivals on the page, c of which continued to another page, x of which had no referrer.
Panel view (Kirby ≥ 4)
Since v1.5.0 the Panel gets an Audience entry in its menu (/panel/audience). It reads site/logs/moniter/stats.sqlite directly on the site: it does not need Moniter and shows today's views right away.
- Key figures for 7 days, 30 days, this month, 90 days or 1 year, compared to the previous period of the same length (only when that period was fully measured).
- Page views per day (per month over 1 year), most read pages, entry pages and bounce, sources, referring sites, devices, languages, clicks, campaigns.
- Page views per country on a world map (Natural Earth 110m, public domain,
assets/world-110m.json, loaded only when there are countries). - Same calculations and same look as the Moniter dashboard and its monthly report (cream, ink bars, coloured cards). Follows the Panel's light / dark theme on Kirby 5.
- Labels in French and English (other Panel languages fall back to English).
return [ 'moniter.panel' => true, // false to hide it, or a list of roles: ['admin', 'editor'] ];
Kirby 3 sites keep working, without the Panel view.
Works with 'panel' => ['vue' => ['compiler' => false]] (a CSP without 'unsafe-eval') since v1.5.1: the view ships precompiled render functions. Before that it rendered a blank page there, with no error.
Changing the view
The Panel code lives in src/panel.js, written with ordinary template strings. index.js is generated from it - edit the source, then:
npm install
npm run build # scripts/build-panel.mjs → index.js, commit both
Security
- Key comparison uses
hash_equals()(timing attack protection) - Without a valid
X-Moniter-Keyheader, the endpoint always returns401 - Never commit the key in the client site repository — use an environment variable if needed
Requirements
- Kirby 4.x or 5.x
- PHP 8.0+
License
MIT — rollinger.design