Search by

rllngr / kirby-moniter

RLLNGR

Kirby CMS plugin — exposes a protected status endpoint for the Moniter dashboard

Package info

github.com/RLLNGR/kirby-moniter

Type:kirby-plugin

pkg:composer/rllngr/kirby-moniter

Statistics

Installs: 140

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.7.0 2026-10-30 14:01 UTC

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 update of 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.languages list (['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 (UTC in 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 sessionStorage for 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 (or utm_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 a script-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 (needs pdo_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-Key header, the endpoint always returns 401
  • 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