@uniweb/core 0.8.4 → 0.8.5
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.
- package/package.json +2 -2
- package/src/website.js +56 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@uniweb/theming": "^0.1.15",
|
|
39
|
-
"@uniweb/semantic-parser": "^1.2.
|
|
39
|
+
"@uniweb/semantic-parser": "^1.2.2"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"test": "vitest run"
|
package/src/website.js
CHANGED
|
@@ -13,6 +13,27 @@ import { normalizeSeo } from './seo.js'
|
|
|
13
13
|
import { resolveDefaultLocale } from './locale-config.js'
|
|
14
14
|
import { matchDynamicRoute } from './route-match.js'
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Decode a route that arrived from a URL.
|
|
18
|
+
*
|
|
19
|
+
* `location.pathname` is percent-encoded per RFC 3986, while page routes and
|
|
20
|
+
* `i18n.routeTranslations` are authored as plain text — so the two are only
|
|
21
|
+
* comparable once the incoming side is decoded.
|
|
22
|
+
*
|
|
23
|
+
* Guarded rather than bare: a route may legitimately contain a `%` that is not
|
|
24
|
+
* an escape (`/100%-Guide` authored by hand, or a value already decoded once),
|
|
25
|
+
* and `decodeURIComponent` throws `URIError` on those. Falling back to the input
|
|
26
|
+
* keeps a malformed route matching exactly as well as it did before.
|
|
27
|
+
*/
|
|
28
|
+
function decodeRoute(route) {
|
|
29
|
+
if (typeof route !== 'string' || !route.includes('%')) return route
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(route)
|
|
32
|
+
} catch {
|
|
33
|
+
return route
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* Website — orchestration root for a single site instance.
|
|
18
39
|
*
|
|
@@ -321,16 +342,32 @@ export default class Website {
|
|
|
321
342
|
if (!locale || locale === this.siteDefaultLocale) return displayRoute
|
|
322
343
|
const entry = this._routeTranslations[locale]
|
|
323
344
|
if (!entry) return displayRoute
|
|
345
|
+
|
|
346
|
+
// The caller hands us a route that came from a URL, and a browser
|
|
347
|
+
// percent-encodes everything outside the unreserved set — so a French slug
|
|
348
|
+
// arrives as `/Sites-Web/Th%C3%A8me-du-site-Web`. The translation map is
|
|
349
|
+
// built from site.yml, where it is authored as plain text. Decoding here
|
|
350
|
+
// rather than at each call site is deliberate: getPage(), normalizeRoute()
|
|
351
|
+
// and getLocaleUrl() all feed this, and a future caller would have to
|
|
352
|
+
// remember otherwise.
|
|
353
|
+
//
|
|
354
|
+
// Without it translateRoute() emits a URL this method cannot read back —
|
|
355
|
+
// every translated route carrying a non-ASCII character or an apostrophe
|
|
356
|
+
// resolved to nothing and rendered the 404 page, while the SAME route with
|
|
357
|
+
// an all-ASCII slug worked. `route-match.js` already decodes captured
|
|
358
|
+
// params for exactly this reason.
|
|
359
|
+
const route = decodeRoute(displayRoute)
|
|
360
|
+
|
|
324
361
|
// Exact match
|
|
325
|
-
const canonical = entry.reverse.get(
|
|
362
|
+
const canonical = entry.reverse.get(route)
|
|
326
363
|
if (canonical) return canonical
|
|
327
364
|
// Prefix match
|
|
328
365
|
for (const [trans, canon] of entry.reverse) {
|
|
329
|
-
if (
|
|
330
|
-
return canon +
|
|
366
|
+
if (route.startsWith(trans + '/')) {
|
|
367
|
+
return canon + route.slice(trans.length)
|
|
331
368
|
}
|
|
332
369
|
}
|
|
333
|
-
return
|
|
370
|
+
return route
|
|
334
371
|
}
|
|
335
372
|
|
|
336
373
|
/**
|
|
@@ -412,7 +449,10 @@ export default class Website {
|
|
|
412
449
|
// Strip locale prefix if present (e.g., '/fr/about' → '/about')
|
|
413
450
|
// Pages are stored with non-prefixed routes; the locale is a URL concern,
|
|
414
451
|
// not a page identity concern.
|
|
415
|
-
|
|
452
|
+
// Decode before ANY comparison: a published payload can hold translated
|
|
453
|
+
// display routes verbatim, so the direct match below needs the same plain
|
|
454
|
+
// text form the reverse-translate path does.
|
|
455
|
+
let stripped = decodeRoute(route)
|
|
416
456
|
if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
|
|
417
457
|
const prefix = `/${this.activeLocale}`
|
|
418
458
|
if (stripped === prefix || stripped === `${prefix}/`) {
|
|
@@ -440,10 +480,17 @@ export default class Website {
|
|
|
440
480
|
}
|
|
441
481
|
|
|
442
482
|
// Reverse-translate display route to canonical (e.g., '/acerca-de' → '/about')
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
//
|
|
446
|
-
//
|
|
483
|
+
//
|
|
484
|
+
// Feed it the TRAILING-SLASH-NORMALIZED form. The translation map is keyed
|
|
485
|
+
// without a trailing slash, so `/acerca-de/` missed the exact lookup and
|
|
486
|
+
// fell through to the prefix branch, which rewrites only the FIRST segment
|
|
487
|
+
// — `/blogue/mi-articulo/` became `/blog/mi-articulo/`, leaving the child
|
|
488
|
+
// segment untranslated and pointing at no page. It looked like it worked
|
|
489
|
+
// for as long as every child slug happened to be identical in both locales.
|
|
490
|
+
stripped = this.reverseTranslateRoute(normalizedStripped)
|
|
491
|
+
|
|
492
|
+
// A translation VALUE may itself carry a trailing slash, so normalize again
|
|
493
|
+
// rather than assuming the input normalization covered it.
|
|
447
494
|
const normalizedRoute = stripped === '/' ? '/' : stripped.replace(/\/$/, '')
|
|
448
495
|
|
|
449
496
|
// Priority 1b: Exact match on canonical route
|