@uniweb/core 0.26.0 → 0.26.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,8 +44,8 @@
44
44
  "vitest": "^4.1.7"
45
45
  },
46
46
  "dependencies": {
47
- "@uniweb/theming": "^0.1.15",
48
- "@uniweb/semantic-parser": "^1.4.0"
47
+ "@uniweb/semantic-parser": "^1.4.0",
48
+ "@uniweb/theming": "^0.1.15"
49
49
  },
50
50
  "scripts": {
51
51
  "test": "vitest run"
package/src/index.js CHANGED
@@ -71,6 +71,7 @@ export {
71
71
  routeParamName,
72
72
  parentRouteOf,
73
73
  recordRouteBase,
74
+ findPageForRoute,
74
75
  } from './route-match.js'
75
76
  export { isRichSchema } from './schemas.js'
76
77
  // ⛔ `Tracker` is NOT on the package entry. It is a FEATURE, not part of the
package/src/page.js CHANGED
@@ -600,11 +600,23 @@ export default class Page {
600
600
  * is "give me the page to render *here*," not "give me the next URL
601
601
  * with content."
602
602
  *
603
- * Used by SSR paths (Cloudflare Worker isolate today; framework SSG
604
- * pre-handles this differently via expandDynamicPages) to avoid
605
- * rendering nothing when an author requests `/docs` and the page is
606
- * a folder whose actual landing content lives in `/docs/intro` flagged
607
- * as `isIndex: true`.
603
+ * The case it exists for: an author requests `/docs`, the page is a folder
604
+ * with no content of its own, and its actual landing content lives in
605
+ * `/docs/intro` flagged `isIndex: true`.
606
+ *
607
+ * ⭐ **ONE CALLER, AND IT IS THE RULE FOR BOTH OF THE RENDERER'S TARGET FORMS:**
608
+ * `createPageRenderer().render()` (`@uniweb/runtime`) applies this after
609
+ * resolving, so a route and an already-resolved Page promote identically.
610
+ * Ruled 2026-09-12 [Diego], closing an asymmetry in which the same page rendered
611
+ * differently depending on which form the caller held — silently, with no error
612
+ * on either path.
613
+ *
614
+ * ⚠️ **It briefly had NO caller** (2026-09-12), after an SSR consumer replaced its
615
+ * hand-composed sequence with `render()`. An earlier version of this paragraph
616
+ * named that consumer and claimed the static build "pre-handles this differently
617
+ * via expandDynamicPages"; **both halves were wrong** — the static build never
618
+ * called this at all: it emits a redirect to the first descendant with content
619
+ * (`getNavigableRoute`) rather than promoting anything.
608
620
  *
609
621
  * @returns {Page} The page to render — `this` or its index child.
610
622
  */
@@ -556,3 +556,49 @@ export function stripLocalePrefix(route, activeLocale, defaultLocale) {
556
556
  if (route.startsWith(`${prefix}/`)) return route.slice(prefix.length)
557
557
  return route
558
558
  }
559
+
560
+ /**
561
+ * ⭐ **THE PAGE A ROUTE NAMES — one rule, for every lane that asks the question.**
562
+ *
563
+ * Exact match first, then the parametric pages in page order, which is the order
564
+ * `Website#getPage` uses in the SPA. Captured params are decoded by
565
+ * `matchDynamicRoute` (a catch-all per segment), so the values are what a site's
566
+ * query binds against.
567
+ *
568
+ * ⛔ **Why this is a leaf rather than a thing each lane composes.** More than one
569
+ * lane asks *which page does this path name* — the SPA, the runtime's server-side
570
+ * prefetch, and a host's router. Our leaves (`normalizeRoute`, `isDynamicRoute`,
571
+ * `routePatternToRegex`) were importable and the RULE was not, so a consumer
572
+ * imported the parts and wrote the composition itself. **A renamed export breaks
573
+ * loudly; a re-implemented rule drifts silently.** A hand-written matcher spelled
574
+ * `:(\w+)` accepts a different param alphabet than our `[A-Za-z0-9_-]+`, so a
575
+ * hyphenated slug resolves in one copy and 404s in the other, with nothing failing
576
+ * anywhere. Import this; do not rebuild it.
577
+ *
578
+ * ⚠️ **A trailing slash is the same route, on BOTH branches** (`normalizeRoute`),
579
+ * which is what `Website#getPage` has always done. The runtime's own prefetch copy
580
+ * compared raw strings until 2026-09-12 and so disagreed with the SPA about
581
+ * `/about/` — the exact class of split this function exists to end.
582
+ *
583
+ * ⚖️ Matching is not resolving: a hit means the ROUTE exists. Whether the record
584
+ * behind a parametric page exists is a data question answered later.
585
+ *
586
+ * @param {Array<Object>|{pages?: Array<Object>}} source - the pages, or a payload holding them
587
+ * @param {string} route - the concrete path
588
+ * @returns {{ page: Object|null, params: Record<string,string> }} `page` is null when nothing matches
589
+ */
590
+ export function findPageForRoute(source, route) {
591
+ const pages = Array.isArray(source) ? source : source?.pages
592
+ const list = Array.isArray(pages) ? pages : []
593
+ const wanted = normalizeRoute(route)
594
+
595
+ for (const page of list) {
596
+ if (typeof page?.route === 'string' && normalizeRoute(page.route) === wanted) return { page, params: {} }
597
+ }
598
+ for (const page of list) {
599
+ if (typeof page?.route !== 'string' || !isDynamicRoute(page.route)) continue
600
+ const hit = matchDynamicRoute(page.route, wanted)
601
+ if (hit) return { page, params: hit.params }
602
+ }
603
+ return { page: null, params: {} }
604
+ }