Index: src/routes/admin-stats.js
===================================================================
--- src/routes/admin-stats.js	(revision d5495493691e5642bb7df668ba1fd87506fdd369)
+++ src/routes/admin-stats.js	(revision d5495493691e5642bb7df668ba1fd87506fdd369)
@@ -0,0 +1,30 @@
+/**
+ * Admin: Statistieken (premium-module, god-only).
+ *
+ * GET /admin/stats -> cookievrije statistieken: bezoekers/weergaven per dag,
+ *                     plays, en de populairste posts/tracks.
+ *
+ * Premium-gated via premiumUnlocked() (premium-laag uit = gewoon beschikbaar;
+ * aan = Patreon vereist). Tracking zit in StatsService (geen cookies).
+ */
+
+import express from 'express';
+import { renderPage } from '../middleware/render.js';
+import { requireGod } from '../middleware/auth.js';
+import { premiumUnlocked } from '../services/PatreonService.js';
+import { getStats } from '../services/StatsService.js';
+
+const router = express.Router();
+
+router.get('/', requireGod, (req, res) => {
+  if (!premiumUnlocked()) {
+    return res.status(403).send('Statistieken is een premium-functie — koppel Patreon in Beheer → Instellingen.');
+  }
+  renderPage(req, res, 'pages/admin-stats', {
+    pageTitle: 'Statistieken',
+    bodyClass: 'on-admin',
+    stats: getStats(14),
+  });
+});
+
+export default router;
Index: src/routes/audio.js
===================================================================
--- src/routes/audio.js	(revision 8ea3d0d20c8b60c9d9a13843b3b654f0e0a7bfc0)
+++ src/routes/audio.js	(revision d5495493691e5642bb7df668ba1fd87506fdd369)
@@ -28,4 +28,6 @@
 import path from 'path';
 import { fileURLToPath } from 'url';
+import db from '../config/database.js';
+import { recordPlay } from '../services/StatsService.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -90,4 +92,17 @@
   const range = req.headers.range;
 
+  // Statistieken: tel één play bij de initiële player-fetch (niet bij scrub/
+  // range-continuaties; replays binnen 24u komen uit de browsercache → geen
+  // dubbeltelling). Best-effort, mag nooit de stream breken.
+  if (req.get('X-Audio-Player') === '1' && (!range || /^bytes=0-/.test(range))) {
+    try {
+      const tr = db.prepare(`
+        SELECT t.id FROM audio_tracks t JOIN media m ON t.media_id = m.id
+        WHERE m.storage_path = ? OR m.storage_path LIKE ? LIMIT 1
+      `).get(filename, '%' + filename);
+      if (tr) recordPlay(tr.id);
+    } catch {}
+  }
+
   // Common headers
   res.setHeader('Content-Type', mime);
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 8ea3d0d20c8b60c9d9a13843b3b654f0e0a7bfc0)
+++ src/routes/posts.js	(revision d5495493691e5642bb7df668ba1fd87506fdd369)
@@ -8,4 +8,5 @@
 import { requireAuth } from '../middleware/auth.js';
 import { renderPage } from '../middleware/render.js';
+import { recordPageview, recordPostView } from '../services/StatsService.js';
 import PermissionsService from '../services/PermissionsService.js';
 import MarkdownService from '../services/MarkdownService.js';
@@ -113,4 +114,6 @@
   `).all(site.id);
 
+  recordPageview(site.id, req);
+
   renderPage(req, res, 'pages/home', {
     pinnedPosts,
@@ -391,4 +394,7 @@
     if (!canEdit) return res.status(403).send('Not published');
   }
+
+  // Statistieken: tel de weergave (skipt beheerders + niet-gepubliceerd-eigen-preview).
+  if (post.status === 'published') recordPostView(post, req);
 
   // Render content. Content is now user-authored HTML (already sanitized on
