mnfst 0.5.162 → 0.5.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,10 @@
1
- /* Manifest Payments
1
+ /* Manifest Payments — config
2
2
  /* By Andrew Matlock under MIT license
3
3
  /* https://manifestx.dev
4
- /*
5
- /* Provider-agnostic payments surface (x-pay / $pay).
6
- /* The client only ever talks to YOUR function endpoint — never a provider's
7
- /* secret API directly. Session creation, fulfilment webhooks and any mutation
8
- /* live server-side. See manifest.payments.core.js for the contract.
9
4
  */
10
5
 
11
- /* Payments config */
12
-
13
- // Refuse strings still containing an unresolved ${VAR}. The loader interpolates
14
- // against window.env before caching the manifest, so a literal ${VAR} here means
15
- // an undefined env var — fail loud rather than POST it to the function verbatim.
6
+ // Refuse strings still containing an unresolved ${VAR} — fail loud rather than
7
+ // POST an undefined env var to the function verbatim.
16
8
  function resolvedOrNull(value, fieldName) {
17
9
  if (typeof value !== 'string') return value;
18
10
  if (/\$\{[^}]+\}/.test(value)) {
@@ -35,13 +27,6 @@ async function ensureManifest() {
35
27
  }
36
28
 
37
29
  // Normalize manifest.payments into a stable config object.
38
- // provider default adapter key (e.g. "revolut", "stripe", "paddle")
39
- // endpoint YOUR function base — mints checkout/portal sessions, verifies webhooks
40
- // mode default modality hint: "redirect" | "overlay" (server may override)
41
- // publicKey publishable key for overlay SDK init (safe to expose; NOT a secret key)
42
- // managed true = Manifest-hosted server moment (endpoint inferred)
43
- // state optional reactive-state source for $pay.state: { url } (GET) — for
44
- // managed mode. Appwrite-backed projects read state via $x instead.
45
30
  let _cache = null;
46
31
  async function getPaymentsConfig() {
47
32
  if (_cache) return _cache;
@@ -71,33 +56,10 @@ async function getPaymentsConfig() {
71
56
  window.ManifestPaymentsConfig = { getPaymentsConfig, ensureManifest };
72
57
 
73
58
 
74
- /* Payments adapters */
75
- //
76
- // An adapter is the ONLY provider-specific client code. It knows how to open a
77
- // provider's overlay/embedded checkout. Link-through (redirect) needs no adapter
78
- // and is the universal floor — ANY provider with a hosted page works without one.
79
- //
80
- // Capability tiers:
81
- // overlay (supportsOverlay: true) — provider exposes a programmatic modal
82
- // with a success/cancel callback. Shipped
83
- // for: revolut, paddle, lemonsqueezy,
84
- // polar, razorpay.
85
- // redirect (supportsOverlay: false) — no no-mount modal; works via the redirect
86
- // floor (server returns {mode:'redirect',
87
- // url}). Embedded/popup variants (Stripe
88
- // Elements, Square Web Payments, PayPal
89
- // Buttons, …) can be added per-provider via
90
- // $pay.register(name, customAdapter).
91
- //
92
- // Adapter shape:
93
- // { supportsOverlay: bool,
94
- // async open({ url, params, config }) -> { status: 'complete'|'cancelled'|… } }
95
- //
96
- // The server (your function) decides modality per response, so adapters never see
97
- // secret keys — only a hosted url, publishable params, or a public token.
59
+ /* Manifest Payments — provider adapters (overlay/embedded checkout) */
98
60
 
99
- // Load a CDN script once; resolve when ready. Adapters call this on first use so
100
- // heavy provider SDKs are never bundled and never appear in prerendered output.
61
+ // Load a CDN script once; resolve when ready. Keeps heavy provider SDKs out of
62
+ // the bundle and prerendered output.
101
63
  const _loaded = {};
102
64
  function loadScript(src, attrs) {
103
65
  if (_loaded[src]) return _loaded[src];
@@ -127,8 +89,7 @@ function list() { return Object.keys(_registry).map(n => ({ name: n, overlay: !!
127
89
 
128
90
  /* ---- Overlay adapters (programmatic modal + success callback) ------------- */
129
91
 
130
- // Revolut Merchant — popup over an order token. Function returns
131
- // { provider:'revolut', params:{ token } } (token = the order's public id).
92
+ // Revolut Merchant — popup over an order token.
132
93
  register('revolut', {
133
94
  supportsOverlay: true,
134
95
  async open({ params = {}, config }) {
@@ -148,8 +109,7 @@ register('revolut', {
148
109
  }
149
110
  });
150
111
 
151
- // Paddle (MoR) — overlay via Paddle.js. Function returns
152
- // { provider:'paddle', params:{ token, items, customer, … } } (token = publishable).
112
+ // Paddle (MoR) — overlay via Paddle.js.
153
113
  register('paddle', {
154
114
  supportsOverlay: true,
155
115
  async open({ params = {}, config }) {
@@ -169,8 +129,7 @@ register('paddle', {
169
129
  }
170
130
  });
171
131
 
172
- // Lemon Squeezy (MoR) — URL-in-a-modal via lemon.js. Function returns
173
- // { provider:'lemonsqueezy', url } (a hosted checkout URL).
132
+ // Lemon Squeezy (MoR) — URL-in-a-modal via lemon.js.
174
133
  register('lemonsqueezy', {
175
134
  supportsOverlay: true,
176
135
  async open({ url }) {
@@ -189,8 +148,7 @@ register('lemonsqueezy', {
189
148
  }
190
149
  });
191
150
 
192
- // Polar (MoR) — URL-in-a-modal via the embed SDK. Function returns
193
- // { provider:'polar', url } (a checkout-link/session url).
151
+ // Polar (MoR) — URL-in-a-modal via the embed SDK.
194
152
  register('polar', {
195
153
  supportsOverlay: true,
196
154
  async open({ url }) {
@@ -206,8 +164,7 @@ register('polar', {
206
164
  }
207
165
  });
208
166
 
209
- // Razorpay — true modal checkout. Function returns
210
- // { provider:'razorpay', params:{ key, order_id, amount, … } }.
167
+ // Razorpay — true modal checkout.
211
168
  register('razorpay', {
212
169
  supportsOverlay: true,
213
170
  async open({ params = {} }) {
@@ -225,34 +182,15 @@ register('razorpay', {
225
182
  });
226
183
 
227
184
  /* ---- Redirect-floor providers --------------------------------------------- */
228
- // Well-known hosted-page providers with no clean no-mount modal. They work today
229
- // via the redirect floor the function returns { mode:'redirect', url } (a hosted
230
- // checkout / payment link / order approve url). Registered for discoverability and
231
- // so a mistaken mode:'overlay' degrades cleanly. Add an embedded/popup variant any
232
- // time with $pay.register(name, { supportsOverlay:true, open }).
233
- //
234
- // stripe Checkout session url (Elements/embedded = custom adapter)
235
- // square Payment Link / Checkout API url (Web Payments SDK = custom)
236
- // paypal Orders API approve url (Smart Buttons popup = custom)
237
- // braintree hosted / Drop-in (Drop-in needs a mount node = custom)
238
- // adyen Pay-by-Link / hosted (Drop-in/Components = custom)
239
- // mollie hosted checkout (redirect-first by design)
240
- //
241
- // NOTE: any provider NOT listed here still works with zero config via the same
242
- // redirect floor (absolute-URL ref, or the function returning {mode:'redirect'}).
243
- // Hosted destinations like Patreon / Buy Me a Coffee / Ko-fi / donation pages are
244
- // just link-through: <a x-pay="'https://patreon.com/you'">…</a>. No adapter needed.
185
+ // Registered for discoverability; no overlay, so they degrade to the redirect
186
+ // floor. Unlisted providers work the same way with zero config.
245
187
  ['stripe', 'square', 'paypal', 'braintree', 'adyen', 'mollie']
246
188
  .forEach((name) => register(name, { supportsOverlay: false, redirectOnly: true }));
247
189
 
248
190
  window.ManifestPaymentsAdapters = { register, get, list, loadScript };
249
191
 
250
192
 
251
- /* Payments store */
252
- //
253
- // Reactive in-flight + server-defined state for $pay. Commerce semantics are NOT
254
- // modelled here — `state` is whatever your function returns (schema-less). The
255
- // core engine (plain JS) mutates these fields via Alpine.store('pay').
193
+ /* Manifest Payments — reactive store for $pay */
256
194
 
257
195
  function initializePaymentsStore() {
258
196
  if (typeof Alpine === 'undefined') return;
@@ -274,7 +212,7 @@ document.addEventListener('alpine:init', () => {
274
212
  window.ManifestPaymentsStore = { initialize: initializePaymentsStore };
275
213
 
276
214
 
277
- /* Payments core */
215
+ /* Manifest Payments core engine (client ↔ function contract) */
278
216
  //
279
217
  // CONTRACT (client → your function):
280
218
  // POST {endpoint} { ref, payload, context }
@@ -283,24 +221,19 @@ window.ManifestPaymentsStore = { initialize: initializePaymentsStore };
283
221
  // → { mode:'overlay', provider, url } URL-in-a-modal overlay
284
222
  // GET {state.url}?workspace=…&user=… → arbitrary entitlement record
285
223
  //
286
- // `ref` is OPAQUEa string only your function understands (price id, plan code,
287
- // cart token, "portal", …). Manifest never interprets it. All commerce semantics,
288
- // secret keys and webhook fulfilment live behind the function. Fulfilment is the
289
- // webhook — never trust this redirect/callback; always reconcile against state.
224
+ // `ref` is opaqueManifest never interprets it. Fulfilment is the webhook, not
225
+ // this redirect/callback; always reconcile against state.
290
226
 
291
227
  const RETURN_PARAM = 'checkout';
292
228
 
293
229
  function store() { return window.Alpine?.store('pay') || null; }
294
230
  function setStore(patch) { const s = store(); if (s) Object.assign(s, patch); }
295
231
 
296
- // Navigation indirection. Defaults to a full-page redirect; override via
297
- // setNavigate() to intercept (SPA router integration, or test harnesses that
298
- // must not actually leave the page).
232
+ // Navigation indirection; override via setNavigate() for SPA routers or tests.
299
233
  let _navigate = (url) => window.location.assign(url);
300
234
  function setNavigate(fn) { if (typeof fn === 'function') _navigate = fn; }
301
235
 
302
- // Identity context, auto-injected so the function knows who/which workspace pays.
303
- // Read from the auth store if the auth plugin is present; absent otherwise.
236
+ // Identity context, auto-injected from the auth store when present.
304
237
  function getContext() {
305
238
  const ctx = {};
306
239
  try {
@@ -323,9 +256,7 @@ async function postSession(config, ref, payload, preferMode) {
323
256
  body: JSON.stringify({ ref, payload: payload || null, context })
324
257
  });
325
258
  if (!res.ok) {
326
- // Surface the function's own human message (it returns { error } /
327
- // { message } strings meant for the shopper) rather than a bare status
328
- // code. Fall back to a friendly generic if the body isn't readable JSON.
259
+ // Surface the function's own { error }/{ message } over a bare status code.
329
260
  let message = '';
330
261
  try {
331
262
  const body = await res.json();
@@ -336,8 +267,8 @@ async function postSession(config, ref, payload, preferMode) {
336
267
  return res.json();
337
268
  }
338
269
 
339
- // Drive a server response into the right modality. Overlay degrades to redirect
340
- // when no adapter supports it — the universal floor, so no author hits a wall.
270
+ // Drive a server response into the right modality; overlay degrades to redirect
271
+ // when no adapter supports it.
341
272
  async function dispatch(response, config) {
342
273
  const mode = response?.mode || 'redirect';
343
274
  if (mode === 'overlay') {
@@ -357,10 +288,8 @@ async function dispatch(response, config) {
357
288
  return { status: 'redirected' };
358
289
  }
359
290
 
360
- // Public: initiate a payment flow for an opaque ref.
361
- // ref string the function understands, OR an absolute URL for a plain
362
- // link-through with no server (the "embed a checkout link today" case).
363
- // payload optional opaque data forwarded to the function (+ optional .context).
291
+ // Public: initiate a payment flow for an opaque ref (or an absolute URL for a
292
+ // zero-server link-through).
364
293
  async function initiate(ref, payload = {}) {
365
294
  setStore({ loading: true, error: null });
366
295
  try {
@@ -386,12 +315,10 @@ async function initiate(ref, payload = {}) {
386
315
  }
387
316
  }
388
317
 
389
- // Convenience: open the billing/customer portal. Just another ref ("portal" by
390
- // default) the function maps to a server-minted portal session.
318
+ // Convenience: open the billing/customer portal (just another ref).
391
319
  function portal(ref = 'portal', payload = {}) { return initiate(ref, payload); }
392
320
 
393
- // Read server-defined entitlement state into $pay.state (managed mode). Appwrite
394
- // projects should read state reactively via $x instead of configuring state.url.
321
+ // Read server-defined entitlement state into $pay.state (managed mode).
395
322
  async function refreshState() {
396
323
  const config = await window.ManifestPaymentsConfig.getPaymentsConfig();
397
324
  if (!config?.state?.url) return null;
@@ -411,15 +338,9 @@ async function refreshState() {
411
338
  }
412
339
  }
413
340
 
414
- // On return from a redirect checkout, settle: the webhook is the source of truth,
415
- // so we re-pull state rather than trusting the success redirect. Strips our marker
416
- // from the URL afterward. Success pages should carry ?checkout=1 (set as the
417
- // function's success_url) so we know to reconcile.
418
- //
419
- // The provider's webhook can land seconds AFTER the buyer does, so a single
420
- // refresh can read pre-payment state. Re-poll on a short backoff to absorb the
421
- // lag — each refresh overwrites $pay.state, so the page settles on the granted
422
- // record without author code.
341
+ // On return from a redirect checkout (success_url carries ?checkout=…), re-pull
342
+ // state and strip the marker. The webhook can land seconds after the buyer, so
343
+ // re-poll on a backoff to absorb the lag; each refresh overwrites $pay.state.
423
344
  const RETURN_POLL_MS = [2000, 5000, 10000];
424
345
  function handleReturn() {
425
346
  try {
@@ -439,19 +360,13 @@ function handleReturn() {
439
360
  window.ManifestPayments = { initiate, portal, refreshState, handleReturn, getContext, setNavigate };
440
361
 
441
362
 
442
- /* Payments magic ($pay) */
363
+ /* Manifest Payments — $pay magic (callable + reactive props) */
443
364
  //
444
- // $pay is a callable magic with reactive properties:
445
- // $pay('pro-monthly') initiate a flow for an opaque ref → Promise
446
- // $pay('https://buy.…') absolute URL → plain link-through, no server
365
+ // $pay(ref|url) initiate a flow Promise
447
366
  // $pay.portal() open the billing/customer portal
448
367
  // $pay.refresh() re-pull server state into $pay.state
449
- // $pay.register(name, adapter) add/override an overlay adapter (escape hatch)
450
- // $pay.state server-defined entitlement record (schema-less)
451
- // $pay.loading / $pay.error / $pay.last
452
- //
453
- // Reads of .state/.loading/etc. go through the reactive 'pay' store, so Alpine
454
- // tracks them in x-show / x-text. State for Appwrite projects is read via $x.
368
+ // $pay.register(name, adapter) add/override an overlay adapter
369
+ // $pay.state / .loading / .error / .last reactive (tracked in x-show/x-text)
455
370
 
456
371
  function initializePaymentsMagic() {
457
372
  if (typeof Alpine === 'undefined') return;
@@ -481,19 +396,11 @@ document.addEventListener('alpine:init', () => {
481
396
  window.ManifestPaymentsMagic = { initialize: initializePaymentsMagic };
482
397
 
483
398
 
484
- /* Payments directive (x-pay) */
485
- //
486
- // Sugar for "on click, initiate this ref". Markup is provider- AND modality-
487
- // agnostic — switching either is a config/server change, never an HTML change.
488
- // <button x-pay="'pro-monthly'">Subscribe</button>
489
- // <button x-pay="'credits-1000'">Buy credits</button>
490
- // <button x-pay.portal>Manage billing</button>
491
- // <button x-pay.overlay="'pro-monthly'">Subscribe</button> modality hint
492
- // <a x-pay="cart.token">Checkout</a>
399
+ /* Manifest Payments x-pay directive (click → initiate a ref) */
493
400
  //
494
401
  // Modifiers:
495
- // .portal treat as a portal flow (ref defaults to "portal")
496
- // .overlay/.redirect hint preferred modality to the function (server decides)
402
+ // .portal portal flow (ref defaults to "portal")
403
+ // .overlay/.redirect hint preferred modality (server decides)
497
404
 
498
405
  function initializePaymentsDirective() {
499
406
  if (typeof Alpine === 'undefined') return;
@@ -508,11 +415,9 @@ function initializePaymentsDirective() {
508
415
  const hasExpr = expression && expression.trim().length > 0;
509
416
  const getRef = hasExpr ? evaluateLater(expression) : null;
510
417
 
511
- // Busy guard: ignore clicks while a flow is in flight and disable the
512
- // element, so double-clicks can't mint duplicate checkout sessions.
513
- // On failure, surface feedback automatically via the toasts plugin
514
- // when it's loaded ($toast resolved through this element's Alpine
515
- // scope); programmatic $pay() callers handle their own rejections.
418
+ // Busy guard: ignore clicks and disable the element while in flight, so
419
+ // double-clicks can't mint duplicate sessions. On failure, surface a
420
+ // toast if the toasts plugin is loaded.
516
421
  const run = async (ref) => {
517
422
  if (el.getAttribute('aria-busy') === 'true') return;
518
423
  const payload = modeHint ? { mode: modeHint } : {};
@@ -556,11 +461,7 @@ document.addEventListener('alpine:init', () => {
556
461
  window.ManifestPaymentsDirective = { initialize: initializePaymentsDirective };
557
462
 
558
463
 
559
- /* Payments main */
560
- //
561
- // Orchestration: settle redirect-returns and pull initial state once Alpine and
562
- // (optionally) auth are ready. Registration of store/magic/directive happens in
563
- // their own subscripts on alpine:init.
464
+ /* Manifest Payments boot (settle redirect-returns, pull initial state) */
564
465
 
565
466
  function bootPayments() {
566
467
  if (window.__manifestPaymentsBooted) return;
@@ -124,12 +124,11 @@ let currentRoute = '/';
124
124
  let isInternalNavigation = false;
125
125
 
126
126
  function isPrerenderedStaticBuild() {
127
- // When prerendered HTML is served as static pages, prefer normal browser navigation (MPA)
128
- // so each URL loads its own prerendered HTML rather than SPA toggling.
127
+ // Prerendered static pages navigate MPA-style (each URL loads its own HTML).
129
128
  const prerendered = document.querySelector('meta[name="manifest:prerendered"]');
130
129
  const val = (prerendered?.getAttribute('content') || '').trim().toLowerCase();
131
130
  if (prerendered && val !== '0' && val !== 'false') return true;
132
- // Backstop: prerender writes this per-page depth marker.
131
+ // Backstop: per-page depth marker written by prerender.
133
132
  return !!document.querySelector('meta[name="manifest:router-base-depth"]');
134
133
  }
135
134
 
@@ -326,17 +325,12 @@ async function handleRouteChange() {
326
325
  const prevRoute = currentRoute;
327
326
  currentRoute = newRoute;
328
327
 
329
- // Handle scrolling based on whether this is an anchor link or route change
328
+ // Route change scrolls to top; anchor links let the browser scroll naturally.
330
329
  if (!window.location.hash) {
331
- // This is a route change - scroll to top
332
- // Use a small delay to ensure content has loaded
333
330
  setTimeout(() => {
334
- // Scroll main page to top
335
331
  window.scrollTo({ top: 0, behavior: 'smooth' });
336
332
 
337
- // Find and scroll scrollable containers to top
338
- // Use a generic approach that works with any CSS framework
339
- // Only check elements that are likely to be scrollable containers
333
+ // Also reset any scrollable containers.
340
334
  const potentialContainers = document.querySelectorAll('div, main, section, article, aside, nav, header, footer, .prose');
341
335
  potentialContainers.forEach(element => {
342
336
  const computedStyle = window.getComputedStyle(element);
@@ -353,11 +347,8 @@ async function handleRouteChange() {
353
347
  });
354
348
  }, 50);
355
349
  } else {
356
- // This is an anchor link - let the browser handle the scroll naturally
357
- // Use a small delay to ensure content has loaded, then let browser scroll to anchor
358
350
  setTimeout(() => {
359
- // The browser will automatically scroll to the anchor
360
- // We just need to ensure the content is loaded first
351
+ // Let the browser scroll to the anchor once content has loaded.
361
352
  }, 50);
362
353
  }
363
354
 
@@ -370,18 +361,9 @@ async function handleRouteChange() {
370
361
  }
371
362
  });
372
363
 
373
- // SPA route changes use the View Transitions API when available so the
374
- // visibility-toggle that listeners perform inside this dispatch is
375
- // animated. Cross-document MPA navigations are already handled by
376
- // `@view-transition { navigation: auto }` in the framework's reset CSS;
377
- // the same `::view-transition-group(*)` rule (driven by
378
- // `--view-transition-duration` / `--view-transition-easing`) covers both.
379
- //
380
- // The callback is synchronous: visibility/head/anchor listeners mutate
381
- // the DOM inside `dispatchEvent` and return. Returning anything async
382
- // here would freeze the rendered frame until the promise resolves,
383
- // adding the entirety of Alpine's pending-update queue to the perceived
384
- // navigation time (1–2s on busy pages).
364
+ // Wrap the SPA dispatch in a View Transition when available (MPA is handled
365
+ // by @view-transition in reset CSS). Callback must stay synchronous:
366
+ // returning a promise freezes the frame until Alpine's update queue drains.
385
367
  if (shouldUseViewTransition()) {
386
368
  document.startViewTransition(() => {
387
369
  window.dispatchEvent(event);
@@ -391,30 +373,12 @@ async function handleRouteChange() {
391
373
  }
392
374
  }
393
375
 
394
- // Decide whether SPA route changes should run inside a View Transition.
395
- // Three modes, in priority order:
396
- //
397
- // 1. `<html data-no-view-transitions>` → force OFF
398
- // 2. `<html data-view-transitions>` → force ON
399
- // 3. (neither) → auto: ON when the current page is
400
- // under VT_AUTO_THRESHOLD elements,
401
- // OFF otherwise
402
- //
403
- // The auto threshold exists because the View Transitions API rasterizes the
404
- // full viewport for the "before" and "after" snapshots; cost scales linearly
405
- // with DOM size and gets noticeable above a few thousand elements (a 10k-
406
- // element page measured ~500ms per snapshot in dev). Light pages keep the
407
- // crossfade; heavy pages stay fast.
408
- //
409
- // Cross-document (MPA) navigations are unaffected — those use the browser's
410
- // native cross-document path (`@view-transition { navigation: auto }`),
411
- // which rasterizes in parallel with page load and doesn't expose the cost.
412
- //
413
- // Per-element opt-out (`data-no-view-transition`, singular) on individual
414
- // elements is handled by the existing reset CSS rule that sets
415
- // `view-transition-name: none` on them. `prefers-reduced-motion` is
416
- // respected automatically — the browser falls back to a snap with no
417
- // animation when the user has it set.
376
+ // Whether SPA route changes run inside a View Transition. Priority:
377
+ // data-no-view-transitions → off, data-view-transitions on, else auto
378
+ // (on under VT_AUTO_THRESHOLD elements). Auto threshold: VT rasterizes the
379
+ // full viewport per snapshot, so cost scales with DOM size (~500ms/snapshot
380
+ // on a 10k-element page). Per-element opt-out and prefers-reduced-motion are
381
+ // handled in reset CSS / by the browser.
418
382
  const VT_AUTO_THRESHOLD = 3000;
419
383
 
420
384
  function shouldUseViewTransition() {
@@ -431,14 +395,9 @@ function shouldUseViewTransition() {
431
395
  }
432
396
  }
433
397
 
434
- // Headless automation (Puppeteer / Playwright / Selenium / WebDriver-driven
435
- // Chromium) frequently captures screenshots mid-transition, producing blank
436
- // frames. Real browsers report `navigator.webdriver === false` — including
437
- // when DevTools is open, when launched via `open <url>`, or on mobile — so
438
- // this check exclusively affects automation tooling and leaves end-user
439
- // behavior identical. Authors who want transitions in their automation tests
440
- // can force them on with `<html data-view-transitions>`, which takes priority
441
- // over this attribute via `shouldUseViewTransition()` above.
398
+ // Disable transitions under WebDriver automation, which captures screenshots
399
+ // mid-transition and gets blank frames. Only affects automation
400
+ // (navigator.webdriver); data-view-transitions overrides.
442
401
  if (typeof navigator !== 'undefined' && navigator.webdriver === true) {
443
402
  const html = document.documentElement;
444
403
  if (html && !html.hasAttribute('data-view-transitions')) {
@@ -514,9 +473,8 @@ function installMpaStickyLocaleLinks() {
514
473
  event.preventDefault();
515
474
  event.stopPropagation();
516
475
  event.stopImmediatePropagation();
517
- // For prerendered MPA builds, directory paths must have a trailing slash so that the
518
- // static file host (e.g. Appwrite) resolves them to the correct index.html rather than
519
- // falling back to the root index.html.
476
+ // MPA directory paths need a trailing slash so the static host resolves
477
+ // them to the right index.html, not the root one.
520
478
  const hasFileExt = /\.[a-zA-Z0-9]+$/.test(adjusted);
521
479
  url.pathname = (adjusted !== '/' && !hasFileExt && !adjusted.endsWith('/'))
522
480
  ? adjusted + '/'
@@ -547,18 +505,11 @@ function interceptLinkClicks() {
547
505
  // Handle pure anchor links normally - don't intercept them
548
506
  if (href.startsWith('#')) return;
549
507
 
550
- // Don't intercept blob: or data: URLs. The export plugin creates a
551
- // throwaway <a href="blob:…" download="…"> and programmatically
552
- // clicks it to trigger a file save; if the router treats it as a
553
- // SPA link, `new URL("blob:http://localhost/UUID", origin).pathname`
554
- // resolves to "http://localhost/UUID" which then pushState pushes
555
- // as a same-origin path, producing /http://localhost/UUID and
556
- // landing the user on a 404 instead of downloading.
508
+ // Don't intercept blob:/data: (e.g. export plugin's download links) —
509
+ // pushState would turn them into a bogus same-origin path and 404.
557
510
  if (href.startsWith('blob:') || href.startsWith('data:')) return;
558
511
 
559
- // Honor `download` — the link is opting out of SPA navigation in
560
- // favor of letting the browser save its target. Same intent as
561
- // blob:/data:, just expressed via the standard HTML attribute.
512
+ // Honor `download` — same opt-out intent, via the standard attribute.
562
513
  if (link.hasAttribute('download')) return;
563
514
 
564
515
  // Check if it's an external link FIRST (before any other processing)
@@ -651,8 +602,8 @@ function initializeNavigation() {
651
602
  handleRouteChange();
652
603
  }
653
604
 
654
- // Match the browser URL as soon as this module loads. Later chunks in the same bundle (e.g. router magic)
655
- // may initialize before DOMContentLoaded; getCurrentRoute() must not stay at '/' or $route breaks article pages.
605
+ // Match the browser URL at module load: later chunks (e.g. router magic) may
606
+ // read getCurrentRoute() before DOMContentLoaded; stale '/' breaks $route.
656
607
  currentRoute = pathnameToLogical(window.location.pathname);
657
608
 
658
609
  // Run immediately if DOM is ready, otherwise wait