mnfst 0.5.42 → 0.5.44

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.
@@ -228,6 +228,82 @@ function shouldSkipStickyLocaleForLogicalSegments(segments) {
228
228
  return STICKY_LOCALE_STATIC_FILE_EXT.has(ext);
229
229
  }
230
230
 
231
+ // manifest.prerender.localeRouteExclude → JSON array in meta; logical path prefixes (after locale).
232
+ function parseLocaleRouteExcludePatterns() {
233
+ const meta = document.querySelector('meta[name="manifest:locale-route-exclude"]');
234
+ const raw = meta?.getAttribute('content') || '';
235
+ if (raw.trim().startsWith('[')) {
236
+ try {
237
+ const parsed = JSON.parse(raw.replace(/"/g, '"'));
238
+ if (Array.isArray(parsed)) {
239
+ return parsed.map((s) => String(s).trim()).filter(Boolean);
240
+ }
241
+ } catch {
242
+ /* fall through */
243
+ }
244
+ }
245
+ const legacy = document.querySelector('meta[name="manifest:locale-sticky-exclude"]');
246
+ const legacyRaw = legacy?.getAttribute('content') || '';
247
+ if (!legacyRaw.trim()) return [];
248
+ return legacyRaw.split(',').map((s) => s.trim().replace(/^\/+/, '').split('/')[0]).filter(Boolean);
249
+ }
250
+
251
+ function logicalPathMatchesLocaleRouteExclude(segments, patterns) {
252
+ if (!patterns.length || !segments.length) return false;
253
+ const lower = segments.map((s) => s.toLowerCase());
254
+ for (const pattern of patterns) {
255
+ const p = String(pattern)
256
+ .trim()
257
+ .replace(/^\/+/, '')
258
+ .split('/')
259
+ .filter(Boolean)
260
+ .map((x) => x.toLowerCase());
261
+ if (p.length === 0) continue;
262
+ if (lower.length < p.length) continue;
263
+ let match = true;
264
+ for (let i = 0; i < p.length; i++) {
265
+ if (lower[i] !== p[i]) {
266
+ match = false;
267
+ break;
268
+ }
269
+ }
270
+ if (match) return true;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ // /fr/legal/terms → /legal/terms when patterns include "legal" or "legal/terms" (prefix match).
276
+ function normalizeRedundantLocalePrefixInUrl() {
277
+ if (!isPrerenderedStaticBuild()) return false;
278
+ const codes = getLocalizationCodesFromManifest();
279
+ const patterns = parseLocaleRouteExcludePatterns();
280
+ if (!codes.length || !patterns.length) return false;
281
+
282
+ const segs = logicalSegmentsFromPathname(window.location.pathname);
283
+ if (segs.length < 2) return false;
284
+ if (!codes.includes(segs[0])) return false;
285
+ const rest = segs.slice(1);
286
+ if (!logicalPathMatchesLocaleRouteExclude(rest, patterns)) return false;
287
+
288
+ const newLogical = '/' + rest.join('/');
289
+ const base = getBasePath();
290
+ let newPathname = base ? base.replace(/\/+$/, '') + newLogical : newLogical;
291
+ newPathname = newPathname.replace(/\/{2,}/g, '/');
292
+ if (!newPathname.startsWith('/')) newPathname = '/' + newPathname;
293
+ if (newPathname === window.location.pathname) return false;
294
+
295
+ const prevLogical = pathnameToLogical(window.location.pathname);
296
+ const u = new URL(window.location.href);
297
+ u.pathname = newPathname;
298
+ history.replaceState(null, '', u.toString());
299
+ currentRoute = pathnameToLogical(newPathname);
300
+ const np = currentRoute === '/' ? '/' : String(currentRoute).replace(/^\/|\/$/g, '');
301
+ window.dispatchEvent(new CustomEvent('manifest:route-change', {
302
+ detail: { from: prevLogical, to: currentRoute, normalizedPath: np }
303
+ }));
304
+ return true;
305
+ }
306
+
231
307
  // When the URL already has a locale prefix (e.g. /zh/pricing), keep it for same-origin links
232
308
  // that omit the prefix (/articles → /zh/articles). No-op on default-locale URLs (/pricing).
233
309
  function applyStickyLocaleToPathname(absolutePathname) {
@@ -242,6 +318,12 @@ function applyStickyLocaleToPathname(absolutePathname) {
242
318
  if (targetSegs.length && codes.includes(targetSegs[0])) {
243
319
  return absolutePathname;
244
320
  }
321
+
322
+ const routeEx = parseLocaleRouteExcludePatterns();
323
+ if (routeEx.length && logicalPathMatchesLocaleRouteExclude(targetSegs, routeEx)) {
324
+ return absolutePathname;
325
+ }
326
+
245
327
  if (shouldSkipStickyLocaleForLogicalSegments(targetSegs)) {
246
328
  return absolutePathname;
247
329
  }
@@ -277,6 +359,7 @@ async function handleRouteChange() {
277
359
  const newRoute = pathnameToLogical(pathname);
278
360
  if (newRoute === currentRoute) return;
279
361
 
362
+ const prevRoute = currentRoute;
280
363
  currentRoute = newRoute;
281
364
 
282
365
  // Handle scrolling based on whether this is an anchor link or route change
@@ -317,7 +400,7 @@ async function handleRouteChange() {
317
400
  // Emit route change event
318
401
  window.dispatchEvent(new CustomEvent('manifest:route-change', {
319
402
  detail: {
320
- from: currentRoute,
403
+ from: prevRoute,
321
404
  to: newRoute,
322
405
  normalizedPath: newRoute === '/' ? '/' : newRoute.replace(/^\/|\/$/g, '')
323
406
  }
@@ -389,9 +472,9 @@ function installMpaStickyLocaleLinks() {
389
472
 
390
473
  const path = url.pathname.replace(/\/$/, '') || '/';
391
474
  const adjusted = applyStickyLocaleToPathname(path);
392
- if (adjusted === path) return;
393
-
394
475
  event.preventDefault();
476
+ event.stopPropagation();
477
+ event.stopImmediatePropagation();
395
478
  url.pathname = adjusted;
396
479
 
397
480
  const dest = url.toString();
@@ -489,8 +572,8 @@ function interceptLinkClicks() {
489
572
 
490
573
  // Initialize navigation
491
574
  function initializeNavigation() {
492
- // Set initial route (logical path for matching)
493
575
  currentRoute = pathnameToLogical(window.location.pathname);
576
+ normalizeRedundantLocalePrefixInUrl();
494
577
 
495
578
  // In prerendered/static output, use default browser navigation (no SPA interception)
496
579
  if (!isPrerenderedStaticBuild()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.42",
3
+ "version": "0.5.44",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",