Index: src/routes/artists.js
===================================================================
--- src/routes/artists.js	(revision 8afbdd6572a947bce4726d5aa0d0aa0d6c7e5067)
+++ src/routes/artists.js	(revision 8afbdd6572a947bce4726d5aa0d0aa0d6c7e5067)
@@ -0,0 +1,65 @@
+/**
+ * Artiesten-directory — alleen in hub-modus.
+ *
+ * GET /artiesten?q=&page=  -> doorzoekbare, gepagineerde lijst van ALLE
+ * PrutFolio's. De hub-home toont maar een beperkte selectie; deze pagina
+ * schaalt naar honderden/duizenden artiesten via zoeken + paginering.
+ *
+ * In solo-modus bestaat er maar één site -> next() (valt door naar postsRoutes,
+ * die 'artiesten' als onbekende slug afhandelt).
+ */
+
+import express from 'express';
+import db from '../config/database.js';
+import { renderPage } from '../middleware/render.js';
+import { getTenancy } from '../services/SettingsService.js';
+
+const router = express.Router();
+
+const PER_PAGE = 24;
+
+router.get('/', (req, res, next) => {
+  if (getTenancy() !== 'hub') return next();
+
+  const q = (req.query.q || '').toString().trim().slice(0, 80);
+  let page = parseInt(req.query.page, 10);
+  if (!Number.isFinite(page) || page < 1) page = 1;
+
+  // Zoekterm tegen titel/slug/tagline (case-insensitive via LIKE; SQLite LIKE is
+  // standaard ongevoelig voor ASCII-hoofdletters). De ESCAPE '\' maakt %, _ en \
+  // in de zoekterm letterlijk (anders zouden ze als wildcards werken).
+  const where = q
+    ? "WHERE (s.title LIKE @like ESCAPE '\\' OR s.slug LIKE @like ESCAPE '\\' OR s.tagline LIKE @like ESCAPE '\\')"
+    : '';
+  const like = '%' + q.replace(/[\\%_]/g, (m) => '\\' + m) + '%';
+  const params = q ? { like } : {};
+
+  const total = db.prepare(`SELECT COUNT(*) AS c FROM sites s ${where}`)
+    .get(params).c;
+  const pages = Math.max(1, Math.ceil(total / PER_PAGE));
+  if (page > pages) page = pages;
+  const offset = (page - 1) * PER_PAGE;
+
+  const artists = db.prepare(`
+    SELECT s.slug, s.title, s.tagline, s.profile_photo, s.accent,
+           u.avatar_url AS owner_avatar,
+           (SELECT COUNT(*) FROM posts WHERE site_id = s.id AND status = 'published') AS post_count
+    FROM sites s
+    LEFT JOIN users u ON u.id = s.owner_id
+    ${where}
+    ORDER BY s.title COLLATE NOCASE ASC
+    LIMIT @limit OFFSET @offset
+  `).all({ ...params, limit: PER_PAGE, offset });
+
+  renderPage(req, res, 'pages/artists-directory', {
+    pageTitle: 'Artiesten',
+    bodyClass: 'on-hub',
+    q,
+    artists,
+    total,
+    page,
+    pages,
+  });
+});
+
+export default router;
