Index: src/server.js
===================================================================
--- src/server.js	(revision 640b39c77691f7bb8ddb40f536fc678f88b8df8d)
+++ src/server.js	(revision 8afbdd6572a947bce4726d5aa0d0aa0d6c7e5067)
@@ -1,4 +1,4 @@
 /**
- * PrutFolio v1 — server bootstrap
+ * Klonkt Hub Beta — server bootstrap
  *
  * Persoonlijk multi-site platform forked van PrutCMS v9 (PHP, file-based).
@@ -21,4 +21,6 @@
 
 import { resolveSite, loadAudioTracks, loadTheme } from './middleware/site.js';
+import { isViewer } from './middleware/auth.js';
+import { renderPage } from './middleware/render.js';
 import authRoutes from './routes/auth.js';
 import accountRoutes from './routes/account.js';
@@ -39,4 +41,5 @@
 import feedRoutes from './routes/feed.js';
 import hubRoutes from './routes/hub.js';
+import artistsRoutes from './routes/artists.js';
 import postsRoutes from './routes/posts.js';
 
@@ -164,10 +167,24 @@
 });
 
-// Read-only/kijk-accounts: alles bekijken mag, niets wijzigen. Blokkeert elke
-// state-wijzigende methode (de login-POST zelf zet de sessie pas, dus die valt
-// hier nog niet onder).
+// Kijker-accounts: alles bekijken mag (incl. Beheer), niets wijzigen. Dit is de
+// ENIGE schrijf-blokkade — fail-closed, vóór alle route-handlers. Elke state-
+// wijzigende methode wordt geweigerd (de login-POST zet de sessie pas ná deze
+// guard, dus die valt er niet onder). I.p.v. rauwe 403-tekst tonen we een nette
+// pagina (of, bij HTMX, een ingeswapte melding).
 app.use((req, res, next) => {
-  if (req.session?.user?.readonly && req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS') {
-    return res.status(403).send('Demo-account: alleen-lezen — wijzigen is uitgeschakeld.');
+  const mutating = req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'OPTIONS';
+  if (mutating && isViewer(req.session?.user)) {
+    if (req.headers['hx-request'] === 'true') {
+      // htmx swapt niet op 4xx; stuur 200 + retarget zodat de melding in #pcms-main verschijnt.
+      res.setHeader('HX-Retarget', '#pcms-main');
+      res.setHeader('HX-Reswap', 'innerHTML');
+      res.status(200);
+    } else {
+      res.status(403);
+    }
+    return renderPage(req, res, 'pages/viewer-blocked', {
+      pageTitle: 'Kijker-modus',
+      bodyClass: 'on-special',
+    });
   }
   next();
@@ -192,4 +209,5 @@
 // Feed/sitemap routes are mounted at root because they're at well-known paths
 app.use('/', feedRoutes);
+app.use('/artiesten', artistsRoutes); // doorzoekbare artiesten-directory (alleen hub; solo: next())
 app.use('/', hubRoutes); // hub-overview op '/' (solo: next() -> postsRoutes)
 app.use('/', postsRoutes);
@@ -215,6 +233,6 @@
   res.json({
     id: idBase,
-    name: site?.title || 'PrutFolio',
-    short_name: (site?.title || 'PrutFolio').slice(0, 12),
+    name: site?.title || 'Klonkt Hub Beta',
+    short_name: (site?.title || 'Klonkt').slice(0, 12),
     description: site?.description || site?.tagline || '',
     scope,
@@ -324,4 +342,12 @@
       return;
     }
+    // Kijker-accounts zijn alleen-lezen: weiger de WS-upgrade. De HTTP-guard
+    // dekt geen WS, dus dit is de plek om schrijven via een (toekomstige)
+    // message-handler te voorkomen.
+    if (isViewer(req.session.user)) {
+      socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+      socket.destroy();
+      return;
+    }
     wss.handleUpgrade(req, socket, head, (ws) => {
       ws.userId = req.session.user.id;
@@ -350,5 +376,5 @@
 server.listen(PORT, () => {
   console.log('');
-  console.log('🪶 PrutFolio v1 — alpha');
+  console.log('🪶 Klonkt Hub Beta');
   console.log(`   http://localhost:${PORT}`);
   console.log('');
