@uniweb/core 0.7.15 → 0.7.17
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 +3 -3
- package/src/entity-store.js +48 -0
- package/src/page.js +13 -5
- package/src/website.js +39 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"vitest": "^4.1.7"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@uniweb/
|
|
34
|
-
"@uniweb/
|
|
33
|
+
"@uniweb/theming": "0.1.6",
|
|
34
|
+
"@uniweb/semantic-parser": "1.1.17"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "vitest run"
|
package/src/entity-store.js
CHANGED
|
@@ -145,6 +145,28 @@ export default class EntityStore {
|
|
|
145
145
|
return configs
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Post-process assembled collection data: for each fetch config that declares a
|
|
150
|
+
* `detailPage` page-ref, resolve it to a locale route template (O(1), via the
|
|
151
|
+
* Website's `_pageIdMap`) and inject a `route` on each record — so a dynamic-list
|
|
152
|
+
* card links to the collection's canonical detail page regardless of which page
|
|
153
|
+
* the list sits on. Runs after `data` is fully assembled, in BOTH the sync (peek)
|
|
154
|
+
* and async (fetch) paths. Replaces the old runtime `getCollectionDetailRoute`
|
|
155
|
+
* page-tree scan. A dangling `detailPage` (unresolvable ref) is a no-op — the
|
|
156
|
+
* component degrades gracefully; records with a baked `route` (file lane) are kept.
|
|
157
|
+
*/
|
|
158
|
+
_applyDetailRoutes(data, configs, website) {
|
|
159
|
+
if (!data || !website?.resolveDetailPageTemplate) return
|
|
160
|
+
for (const [schema, cfg] of configs) {
|
|
161
|
+
if (!cfg.detailPage) continue
|
|
162
|
+
const items = data[schema]
|
|
163
|
+
if (!Array.isArray(items) || items.length === 0) continue
|
|
164
|
+
const template = website.resolveDetailPageTemplate(cfg.detailPage)
|
|
165
|
+
if (!template) continue
|
|
166
|
+
data[schema] = items.map((item) => addDetailRoute(item, template))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
/**
|
|
149
171
|
* Auto-inject `detail:` on collection refs whose collection has
|
|
150
172
|
* `deferred:` declared. The detail pattern points at the per-record
|
|
@@ -363,6 +385,7 @@ export default class EntityStore {
|
|
|
363
385
|
}
|
|
364
386
|
|
|
365
387
|
if (allCached) {
|
|
388
|
+
this._applyDetailRoutes(data, configs, block.website)
|
|
366
389
|
return { status: 'ready', data }
|
|
367
390
|
}
|
|
368
391
|
return { status: 'pending', data: null }
|
|
@@ -465,6 +488,7 @@ export default class EntityStore {
|
|
|
465
488
|
}
|
|
466
489
|
|
|
467
490
|
if (parallelFetches.length > 0) await Promise.all(parallelFetches)
|
|
491
|
+
this._applyDetailRoutes(data, configs, block.website)
|
|
468
492
|
return { data }
|
|
469
493
|
}
|
|
470
494
|
}
|
|
@@ -477,3 +501,27 @@ function peekArray(dispatcher, cfg, ctx) {
|
|
|
477
501
|
if (!cached) return null
|
|
478
502
|
return Array.isArray(cached.data) ? cached.data : null
|
|
479
503
|
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Interpolate a record's fields into a detail-page route template to build its
|
|
507
|
+
* `route` (the canonical href for a card). `/blog/:slug` + `{ slug: 'a-post' }`
|
|
508
|
+
* → `/blog/a-post`. Returns a SHALLOW COPY with `route` added — never mutates the
|
|
509
|
+
* cached record (the same collection may back several sections with different
|
|
510
|
+
* detail pages). Idempotent + back-compat: a record that already carries a `route`
|
|
511
|
+
* (the file lane bakes one via collection-processor) is returned untouched. A
|
|
512
|
+
* `:param` with no matching record field → no `route` (graceful; degrades to the
|
|
513
|
+
* component's own fallback rather than emitting a broken href).
|
|
514
|
+
*/
|
|
515
|
+
function addDetailRoute(item, template) {
|
|
516
|
+
if (!item || typeof item !== 'object' || item.route !== undefined) return item
|
|
517
|
+
let missing = false
|
|
518
|
+
const route = template.replace(/:(\w+)/g, (_, name) => {
|
|
519
|
+
const value = item[name]
|
|
520
|
+
if (value == null) {
|
|
521
|
+
missing = true
|
|
522
|
+
return ''
|
|
523
|
+
}
|
|
524
|
+
return encodeURIComponent(String(value))
|
|
525
|
+
})
|
|
526
|
+
return missing ? item : { ...item, route }
|
|
527
|
+
}
|
package/src/page.js
CHANGED
|
@@ -48,10 +48,16 @@ export default class Page {
|
|
|
48
48
|
// Rewrite target (if set, this route is served by an external site)
|
|
49
49
|
this.rewrite = pageData.rewrite || null
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
51
|
+
// Two orthogonal visibility axes:
|
|
52
|
+
// • `hidden` — REACHABILITY. When true the page is excluded from the published
|
|
53
|
+
// site entirely (the build prunes it and its subtree); it survives only in
|
|
54
|
+
// dev/authoring for preview. A hidden page is also, a fortiori, absent from
|
|
55
|
+
// nav. This is NOT a nav-only flag.
|
|
56
|
+
// • `hideIn` — NAV PLACEMENT. A per-area denylist (layout-area names, e.g.
|
|
57
|
+
// ['header','footer']) applied while the page IS routed. The sentinel '*'
|
|
58
|
+
// means "suppressed from every nav area" (still routed) — this is how a page
|
|
59
|
+
// is kept reachable-but-out-of-all-menus. The legacy hideInHeader/hideInFooter
|
|
60
|
+
// booleans fold into hideIn and are kept as derived accessors for back-compat.
|
|
55
61
|
this.hidden = pageData.hidden || false
|
|
56
62
|
this.hideIn = normalizeHideIn(pageData)
|
|
57
63
|
this.hideInHeader = this.hideIn.includes('header')
|
|
@@ -406,11 +412,13 @@ export default class Page {
|
|
|
406
412
|
/**
|
|
407
413
|
* Check if page should appear in a named nav area ('header', 'footer', or any
|
|
408
414
|
* foundation-declared area). The general form behind showInHeader/showInFooter.
|
|
415
|
+
* False when the page is unpublished (`hidden`), suppressed from every area
|
|
416
|
+
* (`hideIn` contains '*'), or suppressed from this specific area.
|
|
409
417
|
* @param {string} area
|
|
410
418
|
* @returns {boolean}
|
|
411
419
|
*/
|
|
412
420
|
showInNav(area) {
|
|
413
|
-
return !this.hidden && !this.hideIn.includes(area)
|
|
421
|
+
return !this.hidden && !this.hideIn.includes('*') && !this.hideIn.includes(area)
|
|
414
422
|
}
|
|
415
423
|
|
|
416
424
|
/**
|
package/src/website.js
CHANGED
|
@@ -791,6 +791,39 @@ export default class Website {
|
|
|
791
791
|
return resolvedHref
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Resolve a `page:<stable_id>` detail-page reference (from a fetch config's
|
|
796
|
+
* `detailPage`) to a locale-specific route TEMPLATE, e.g. '/blog/:slug'. The
|
|
797
|
+
* entity store interpolates each record's field into the `:param` slot to build
|
|
798
|
+
* a card's href — so a dynamic-list preview links to the collection's canonical
|
|
799
|
+
* detail page regardless of which page it sits on.
|
|
800
|
+
*
|
|
801
|
+
* O(1): a `_pageIdMap` lookup (keyed on stable_id, same map makeHref uses), NOT
|
|
802
|
+
* a page-tree scan. Returns null when the ref is unresolvable — the target page
|
|
803
|
+
* was deleted or de-dynamicized (a dangling ref); the caller degrades gracefully
|
|
804
|
+
* (leaves the record without a `route`). Rename-safe: a stable_id survives page
|
|
805
|
+
* reorganization.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} pageRef - `page:<stable_id>` (bare `<stable_id>` also accepted)
|
|
808
|
+
* @returns {string|null} locale-specific route template, or null if unresolvable
|
|
809
|
+
*/
|
|
810
|
+
resolveDetailPageTemplate(pageRef) {
|
|
811
|
+
if (!pageRef || typeof pageRef !== 'string') return null
|
|
812
|
+
const id = pageRef.startsWith('page:') ? pageRef.slice(5) : pageRef
|
|
813
|
+
const page = this._pageIdMap?.get(id)
|
|
814
|
+
if (!page || !page.route) {
|
|
815
|
+
if (
|
|
816
|
+
typeof console !== 'undefined' &&
|
|
817
|
+
typeof process !== 'undefined' &&
|
|
818
|
+
process?.env?.NODE_ENV !== 'production'
|
|
819
|
+
) {
|
|
820
|
+
console.warn(`[resolveDetailPageTemplate] Detail page not found: ${pageRef}`)
|
|
821
|
+
}
|
|
822
|
+
return null
|
|
823
|
+
}
|
|
824
|
+
return this.translateRoute(page.route)
|
|
825
|
+
}
|
|
826
|
+
|
|
794
827
|
/**
|
|
795
828
|
* Get available languages
|
|
796
829
|
* @deprecated Use getLocales() instead
|
|
@@ -1075,9 +1108,13 @@ export default class Website {
|
|
|
1075
1108
|
|
|
1076
1109
|
// Check visibility based on navigation type
|
|
1077
1110
|
if (!includeHidden) {
|
|
1111
|
+
// `hidden` = unpublished (a fortiori not in nav). In a published build it
|
|
1112
|
+
// is already pruned; this also keeps dev drafts out of the menus.
|
|
1078
1113
|
if (page.hidden) return false
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1114
|
+
// hideIn lists the areas this page is suppressed from; '*' suppresses it
|
|
1115
|
+
// from every area (reachable-but-out-of-all-menus). navType is the
|
|
1116
|
+
// requested area ('header'/'footer'/any foundation-declared area).
|
|
1117
|
+
if (page.hideIn?.includes('*')) return false
|
|
1081
1118
|
if (navType && page.hideIn?.includes(navType)) return false
|
|
1082
1119
|
}
|
|
1083
1120
|
|