@uniweb/kit 0.13.6 → 0.14.0

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/kit",
3
- "version": "0.13.6",
3
+ "version": "0.14.0",
4
4
  "description": "Standard component library for Uniweb foundations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,8 +44,8 @@
44
44
  "shiki": "^3.0.0",
45
45
  "tailwind-merge": "^3.6.0",
46
46
  "temml": "^0.13.2",
47
+ "@uniweb/core": "^0.13.0",
47
48
  "@uniweb/semantic-parser": "^1.3.1",
48
- "@uniweb/core": "^0.12.1",
49
49
  "@uniweb/scene": "^0.1.3"
50
50
  },
51
51
  "peerDependencies": {
@@ -44,7 +44,7 @@
44
44
  * }
45
45
  */
46
46
 
47
- import { getUniweb, recordDataUrl } from '@uniweb/core'
47
+ import { getUniweb, resolveFetchConfigs, buildDetailConfig } from '@uniweb/core'
48
48
  import { useFetched } from './useFetched.js'
49
49
 
50
50
  /**
@@ -60,46 +60,68 @@ import { useFetched } from './useFetched.js'
60
60
  export function useEntityDetail(record, options = {}) {
61
61
  const collection = options?.collection
62
62
  const request = buildDetailRequest(record, collection)
63
- return useFetched(request)
63
+ const result = useFetched(request)
64
+
65
+ // No separate detail source for this collection — nothing was stripped from
66
+ // the cascade, so the record the caller already holds IS the whole record.
67
+ // Returning it beats both alternatives: `null` makes every caller null-check
68
+ // a case that cannot fail, and requesting the per-record file anyway is a
69
+ // guaranteed 404, because that file is only written for a `deferred:`
70
+ // collection. `useFetched` is still called above — unconditionally, as the
71
+ // rules of hooks require — and simply skips on a null request.
72
+ if (record && collection && !request) {
73
+ return { data: record, error: null, loading: false }
74
+ }
75
+ return result
64
76
  }
65
77
 
66
78
  /**
67
79
  * Build the fetch request for one record's full payload.
68
80
  *
69
- * Exported for tests only — not re-exported from the package index. The
70
- * static-file branch has to resolve to the same URL the build writes and the
71
- * same URL core's dynamic-route auto-detail injects; a test can only assert
72
- * that if it can call this without a React tree.
81
+ * ⛔ THIS MUST NOT DECIDE THE ADDRESS ITSELF, and it used to. It read
82
+ * `config.collections[name].detailUrl` directly and otherwise composed
83
+ * `/data/<name>/<slug>.json` by hand, which was wrong three ways: it 404'd on
84
+ * any collection without `deferred:` (that file is only written for one), it
85
+ * could not see a host's live record lane at all, and its hand-rolled `{slug}`
86
+ * replace ignored a route whose param is named anything else.
87
+ *
88
+ * Every one of those is already solved once, in the resolution the runtime and
89
+ * the prerenderer share. So this hands the collection to that resolution and
90
+ * asks it the same question they ask: `resolveFetchConfigs` decides where the
91
+ * collection lives (a host's lane, or the compiled artifact) and what its
92
+ * per-record source is, then `buildDetailConfig` turns that plus a param into a
93
+ * request. A fourth answer computed here is a fourth thing to drift.
94
+ *
95
+ * Returns null when the collection has no separate detail source — the common
96
+ * case, and not a failure. The caller's record is already whole.
97
+ *
98
+ * Exported for tests only — not re-exported from the package index.
73
99
  *
74
100
  * @param {Object|null} record
75
101
  * @param {string} collection
76
- * @returns {{path?: string, url?: string, schema: string}|null}
102
+ * @returns {{path?: string, url?: string, endpoint?: string, schema: string}|null}
77
103
  */
78
104
  export function buildDetailRequest(record, collection) {
79
105
  const slug = record?.slug
80
106
  if (!slug || !collection) return null
81
107
 
82
- // Look up the collection's per-record source pattern. Authors with
83
- // API-backed collections declare `detailUrl:` in site.yml; markdown
84
- // collections leave it null and use the static-file default.
85
108
  const website = getUniweb()?.activeWebsite
86
- const collConfig = website?.config?.collections?.[collection]
87
- const detailUrl = (collConfig && typeof collConfig.detailUrl === 'string')
88
- ? collConfig.detailUrl
89
- : null
109
+ const config = website?.config
90
110
 
91
- if (detailUrl) {
92
- // Substitute {slug} into the author-declared pattern and use url:
93
- // (the source is remote).
94
- const url = detailUrl.replace(/\{slug\}/g, encodeURIComponent(slug))
95
- return { url, schema: collection }
96
- }
111
+ // One synthetic source, resolved by the shared rule — same inputs the
112
+ // EntityStore passes, so the hook cannot disagree with the page it sits on.
113
+ const resolved = resolveFetchConfigs([{ collection, schema: collection }], {
114
+ collections: config?.collections ?? null,
115
+ records: config?.records ?? null,
116
+ locale: website?.getActiveLocale?.() ?? null,
117
+ defaultLocale: website?.getDefaultLocale?.() ?? null,
118
+ }).get(collection)
119
+
120
+ if (!resolved) return null
97
121
 
98
- // Static-file default — the build emitted per-record JSON files. Shares
99
- // `recordDataUrl` with the build that writes them and with core's
100
- // dynamic-route auto-detail injection, which is the other half of this
101
- // same feature: the three must resolve one record to one URL.
102
- return { path: recordDataUrl(collection, slug), schema: collection }
122
+ // `slug` is this hook's contract with its caller (the record must carry one).
123
+ // The `{param}` alias means a host-written record pattern resolves too.
124
+ return buildDetailConfig(resolved, { paramName: 'slug', paramValue: slug })
103
125
  }
104
126
 
105
127
  export default useEntityDetail
@@ -113,7 +113,14 @@ function normalize(raw) {
113
113
  component: raw?.component ?? null,
114
114
  snippetText: raw?.snippetText ?? null,
115
115
  matches: raw?.matches ?? null,
116
- collection: raw?.collection ?? null,
116
+ // ⭐ `group` — the named set a record came from, renamed from `collection`
117
+ // 2026-08-27 with @uniweb/projections. "Collection" is framework's BUILD
118
+ // concept; a hosted site's records come from a folder and have no collection,
119
+ // so the word did not survive the lane it was travelling into.
120
+ // ⚠️ `collection` is still read as a fallback: a search index is a PUBLISHED
121
+ // ARTIFACT, and a site that has not rebuilt since the rename is still serving
122
+ // the old key. Dropping it would blank the field for every such site.
123
+ group: raw?.group ?? raw?.collection ?? null,
117
124
  item: raw?.item ?? null
118
125
  }
119
126
  }
@@ -58,7 +58,7 @@ export function emptyResult() {
58
58
  // Match ranges, when the provider scores locally and can report them.
59
59
  matches: null,
60
60
  // Collection provenance, when the hit is a record rather than a page.
61
- collection: null,
61
+ group: null,
62
62
  item: null
63
63
  }
64
64
  }