Index: src/assets/js/mod/lib.js
===================================================================
--- src/assets/js/mod/lib.js	(revision 59f404ac3fc5229fc0676774f093cbf7eaa661c3)
+++ src/assets/js/mod/lib.js	(revision 369aa60f076533cc58f12e0cff07bbab80962877)
@@ -83,7 +83,23 @@
  * aanroepers hebben al een nette terugval voor "geen WebAuthn hier".
  */
+/**
+ * De cache-buster voor alles in /assets/vendor (shaer-724).
+ *
+ * Die map valt onder dezelfde max-age van een jaar als de rest van /assets, en
+ * een vendorbestand heeft geen versie in zijn naam. Zonder query houdt een
+ * browser die hem ooit zag hem dus een jaar vast, ook na een update van de
+ * bibliotheek.
+ *
+ * EEN nummer voor de hele map, hier, en niet drie losse per module: read.js had
+ * er al een en lib.js en post-edit.js hadden er geen, en dan is "ophogen als de
+ * bibliotheek wijzigt" een opdracht die je op drie plekken moet onthouden. Dat
+ * kost read.js wel een import van dit bestand op de leespagina; die is klein en
+ * staat na de eerste pagina in de cache.
+ */
+export const VENDOR_V = 1;
+
 export async function loadWebAuthn() {
   if (window.SimpleWebAuthnBrowser) return window.SimpleWebAuthnBrowser;
-  try { await import('/assets/vendor/simplewebauthn-browser.umd.min.js'); }
+  try { await import(`/assets/vendor/simplewebauthn-browser.umd.min.js?v=${VENDOR_V}`); }
   catch (e) { console.warn('[paid] WebAuthn-bibliotheek laadt niet:', e && e.message); }
   return window.SimpleWebAuthnBrowser;
Index: src/assets/js/mod/post-edit.js
===================================================================
--- src/assets/js/mod/post-edit.js	(revision 59f404ac3fc5229fc0676774f093cbf7eaa661c3)
+++ src/assets/js/mod/post-edit.js	(revision 369aa60f076533cc58f12e0cff07bbab80962877)
@@ -5,5 +5,5 @@
 // het houdt een apostrof in een vertaling uit de HTML die hier geplakt wordt.
 
-import { pageData, esc, makeSweeper } from './lib.js';
+import { pageData, esc, makeSweeper, VENDOR_V } from './lib.js';
 // Dezelfde regel die de server gebruikt -- zie shared/post-music-type.js voor
 // waarom hij daar staat en niet twee keer.
@@ -173,9 +173,11 @@
       if (!document.querySelector('link[data-cropper-css]')) {
         const l = document.createElement('link');
-        l.rel = 'stylesheet'; l.href = '/assets/vendor/cropper.min.css'; l.setAttribute('data-cropper-css', '');
+        // Met versie, net als het script eronder (shaer-724): /assets wordt een
+        // jaar gecachet en deze twee bestanden dragen geen versie in hun naam.
+        l.rel = 'stylesheet'; l.href = `/assets/vendor/cropper.min.css?v=${VENDOR_V}`; l.setAttribute('data-cropper-css', '');
         document.head.appendChild(l);
       }
       const s = document.createElement('script');
-      s.src = '/assets/vendor/cropper.min.js';
+      s.src = `/assets/vendor/cropper.min.js?v=${VENDOR_V}`;
       s.onload = () => resolve();
       s.onerror = () => reject(new Error('cropper load failed'));
Index: src/assets/js/mod/read.js
===================================================================
--- src/assets/js/mod/read.js	(revision 59f404ac3fc5229fc0676774f093cbf7eaa661c3)
+++ src/assets/js/mod/read.js	(revision 369aa60f076533cc58f12e0cff07bbab80962877)
@@ -30,4 +30,8 @@
  * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.
  */
+
+// VENDOR_V komt uit lib.js: een nummer voor de hele vendormap (shaer-724),
+// zodat 'ophogen als de bibliotheek wijzigt' op een plek staat.
+import { VENDOR_V } from './lib.js';
 
 // ── Paginamodus ─────────────────────────────────────────────────────────────
@@ -294,5 +298,5 @@
 // dan is OP_TOUCH weer false zetten de hele terugweg.
 const OP_TOUCH = window.matchMedia('(hover: none) and (pointer: coarse)');
-const VENDOR_V = 1;   // ophogen als de bestanden in /assets/js/vendor wijzigen
+// VENDOR_V komt uit lib.js: een nummer voor de hele vendormap (shaer-724).
 
 let lenis = null;
