Index: src/server.js
===================================================================
--- src/server.js	(revision 746d98a771aa1e4a4241958d88582d516570cf4b)
+++ src/server.js	(revision b4aaaddef9ee9b6109381addc714faa320110e88)
@@ -17,4 +17,5 @@
 import http from 'http';
 import db, { initializeDatabase } from './config/database.js';
+import ThemeService from './services/ThemeService.js';
 import { startScheduler } from './services/Scheduler.js';
 import { SqliteSessionStore } from './services/SqliteSessionStore.js';
@@ -238,4 +239,15 @@
 
 app.use('/assets', express.static(path.join(__dirname, 'assets'), { maxAge: isDev ? 0 : '1y' }));
+// Lay-outthema's (experimental/theme). De versie staat in het pad, dus een
+// nieuwe versie is een nieuwe URL en deze cache van een jaar klopt altijd --
+// geen ?v= om bij te houden. Alleen wat het pakket zelf noemt wordt geserveerd
+// (ThemeService.layoutFile); een pad uit de URL komt nergens in een join.
+app.get('/themes/:id/:version/:file', (req, res) => {
+  const file = ThemeService.layoutFile(req.params.id, req.params.version, req.params.file);
+  if (!file) return res.status(404).end();
+  res.set('Cache-Control', isDev ? 'no-cache' : 'public, max-age=31536000, immutable');
+  res.set('X-Content-Type-Options', 'nosniff');
+  res.sendFile(file);
+});
 
 // On-demand cover thumbnails: /media/thumb/<w>/<path> → a small lanczos-downscaled WebP
