Index: src/routes/account.js
===================================================================
--- src/routes/account.js	(revision fa089819894c489333fe1d0f327dae1085a86987)
+++ src/routes/account.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -25,4 +25,5 @@
 import { requireAuth } from '../middleware/auth.js';
 import { googleConfigured } from '../config/google.js';
+import { toWebp } from '../services/ImageWebpService.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -190,5 +191,5 @@
     }
 
-    const url = `/media/avatars/${req.file.filename}`;
+    const url = `/media/avatars/${toWebp(req.file)}`;
 
     // Remove the old avatar file (if it lives in our avatar dir)
Index: src/routes/admin-audio.js
===================================================================
--- src/routes/admin-audio.js	(revision fa089819894c489333fe1d0f327dae1085a86987)
+++ src/routes/admin-audio.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -18,4 +18,5 @@
 import db from '../config/database.js';
 import { renderPage } from '../middleware/render.js';
+import { toWebp } from '../services/ImageWebpService.js';
 import { requireGod } from '../middleware/auth.js';
 import { transcodeToMp3 } from '../services/AudioTranscoder.js';
@@ -478,5 +479,5 @@
     }
 
-    const newUrl = `/media/audio-covers/${file.filename}`;
+    const newUrl = `/media/audio-covers/${toWebp(file)}`;
     try {
       db.prepare('UPDATE audio_tracks SET cover_url = ? WHERE id = ? AND site_id = ?')
Index: src/routes/admin-settings.js
===================================================================
--- src/routes/admin-settings.js	(revision fa089819894c489333fe1d0f327dae1085a86987)
+++ src/routes/admin-settings.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -25,4 +25,5 @@
 import { entitlementStatus, premiumUnlocked } from '../services/PatreonService.js';
 import { googleConfigured, redirectUri, currentClientId, clientSecretSet } from '../config/google.js';
+import { toWebp } from '../services/ImageWebpService.js';
 
 const router = express.Router();
@@ -120,5 +121,5 @@
     // Hero: een geüploade afbeelding wint; anders het URL-tekstveld.
     if (req.file) {
-      const newUrl = `/media/hero/${req.file.filename}`;
+      const newUrl = `/media/hero/${toWebp(req.file)}`;
       // Ruim een vorige geüploade hero op (alleen als die uit onze hero-map kwam).
       const old = getSetting('hub_hero_image') || '';
Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision fa089819894c489333fe1d0f327dae1085a86987)
+++ src/routes/admin-sites.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -25,4 +25,5 @@
 import ThemeService from '../services/ThemeService.js';
 import { listPlatforms, PLATFORMS } from '../services/PlatformIcons.js';
+import { toWebp } from '../services/ImageWebpService.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -88,5 +89,5 @@
     res.json({
       ok: true,
-      url: `/media/avatars/${req.file.filename}`,
+      url: `/media/avatars/${toWebp(req.file)}`,
       size: req.file.size,
       mime: req.file.mimetype,
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision fa089819894c489333fe1d0f327dae1085a86987)
+++ src/routes/posts.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -16,4 +16,5 @@
 import PlaylistService from '../services/PlaylistService.js';
 import { audioUrl } from '../services/AudioStreamService.js';
+import { toWebp } from '../services/ImageWebpService.js';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -55,5 +56,5 @@
     if (err) return res.status(400).json({ error: err.message });
     if (!req.file) return res.status(400).json({ error: 'No file' });
-    const url = '/media/post-images/' + req.file.filename;
+    const url = '/media/post-images/' + toWebp(req.file);
     res.json({ url, size: req.file.size, mime: req.file.mimetype });
   });
Index: src/services/ImageWebpService.js
===================================================================
--- src/services/ImageWebpService.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
+++ src/services/ImageWebpService.js	(revision 8f6225cbfb7a439454829f7dec26b0891f4163f0)
@@ -0,0 +1,40 @@
+/**
+ * Zet een zojuist-geüploade afbeelding om naar WebP (kleiner, modern).
+ *
+ * Gebruikt het systeem-`cwebp` (libwebp). Aanwezig → converteer + verwijder het
+ * origineel, geef de nieuwe .webp-bestandsnaam terug. Niet aanwezig of fout →
+ * geef de originele bestandsnaam terug (graceful fallback, niks breekt).
+ *
+ * GIF blijft GIF (cwebp maakt geen geanimeerde webp van een gif); reeds-webp
+ * wordt overgeslagen.
+ */
+import { execFileSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+
+const QUALITY = '82';
+
+/**
+ * @param {{path:string, filename:string, destination?:string}} file  multer file
+ * @returns {string} de definitieve bestandsnaam (basename) — .webp of het origineel
+ */
+export function toWebp(file) {
+  if (!file || !file.path || !file.filename) return file && file.filename;
+  const ext = path.extname(file.filename).toLowerCase();
+  if (ext === '.webp' || ext === '.gif') return file.filename;
+  const dir = file.destination || path.dirname(file.path);
+  const outName = path.basename(file.filename, ext) + '.webp';
+  const outPath = path.join(dir, outName);
+  try {
+    execFileSync('cwebp', ['-quiet', '-q', QUALITY, file.path, '-o', outPath], { stdio: 'ignore' });
+    if (!fs.existsSync(outPath) || fs.statSync(outPath).size === 0) throw new Error('lege output');
+    try { fs.unlinkSync(file.path); } catch { /* origineel weg, niet kritisch */ }
+    return outName;
+  } catch (e) {
+    console.warn('[webp] conversie overgeslagen (cwebp niet beschikbaar/fout):', e.message);
+    try { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); } catch {} // ruim halve output op
+    return file.filename; // behoud origineel
+  }
+}
+
+export default { toWebp };
