Index: src/assets/js/mod/lib.js
===================================================================
--- src/assets/js/mod/lib.js	(revision 4425cc2b8452afe9ea83e6b39cf2f74b34bd2c0f)
+++ 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 4425cc2b8452afe9ea83e6b39cf2f74b34bd2c0f)
+++ 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 4425cc2b8452afe9ea83e6b39cf2f74b34bd2c0f)
+++ 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;
Index: src/views/shell.ejs
===================================================================
--- src/views/shell.ejs	(revision 4425cc2b8452afe9ea83e6b39cf2f74b34bd2c0f)
+++ src/views/shell.ejs	(revision 369aa60f076533cc58f12e0cff07bbab80962877)
@@ -120,4 +120,15 @@
   };
 }
+%>
+<%
+// De cache-buster voor alles onder assets/js/mod (en de gedeelde bestanden die
+// die modules importeren). Stond alleen in de bootstrap onderaan; hij staat nu
+// hier omdat de importmap in de <head> hem OOK nodig heeft, en twee losse
+// nummers lopen gegarandeerd een keer uit elkaar.
+//
+// Ophogen bij ELKE wijziging in assets/js/mod -- /assets wordt buiten
+// ontwikkeling een jaar gecachet, dus zonder bump bereikt een reparatie
+// iedereen behalve wie de fout al heeft.
+const MOD_V = 63;
 %><!DOCTYPE html>
 <html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
@@ -339,4 +350,23 @@
 <script src="/assets/js/htmx.min.js"></script>
 
+<%# De cache-buster reikt ook tot wat de modules ONDERLING importeren (shaer-724).
+    De bootstrap hangt ?v= aan elke module die hij zelf laadt, maar een import
+    BINNEN een module is relatief, en een relatieve specifier erft de query
+    niet: `./lib.js` naast `post.js?v=63` wordt gewoon `/assets/js/mod/lib.js`.
+    Dat bestand wordt met max-age van een jaar geserveerd, dus het bleef bij
+    terugkerende bezoekers staan terwijl de modules eromheen ververst werden --
+    en juist lib.js is gedeeld, dus een fout daarin overleeft elke MOD_V-bump.
+
+    Een importmap lost dat op zonder die elf imports aan te raken: hij vertaalt
+    de OPGELOSTE URL naar zijn geversioneerde vorm. Hij moet in de head staan,
+    voor de eerste module-import. De toets bij dit bestand bewaakt dat elke
+    relatieve import onder assets/js hier een ingang heeft. %>
+<script type="importmap">
+{"imports":{
+  "/assets/js/mod/lib.js": "/assets/js/mod/lib.js?v=<%= MOD_V %>",
+  "/assets/js/shared/post-music-type.js": "/assets/js/shared/post-music-type.js?v=<%= MOD_V %>"
+}}
+</script>
+
 <!-- Per-site custom <head> HTML (analytics, verification, etc.) -->
 <% if (safeSite.custom_head_html) { %>
@@ -505,5 +535,5 @@
   // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig
   // bumpen kost een bugfix die nooit aankomt.
-  var MOD_V = 63;
+  var MOD_V = <%= MOD_V %>;   // uit de EJS-variabele bovenaan; zie de importmap in de head
 
   // name -> 1 (aan het laden) of de module-namespace (geladen). Een module
