@uniweb/projections 0.3.1 → 0.3.3

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/projections",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Projections of a Uniweb site's content — agent index, per-page markdown, search index. Pure JS, runs anywhere.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,8 +31,8 @@
31
31
  "node": ">=20.19"
32
32
  },
33
33
  "dependencies": {
34
- "@uniweb/content-writer": "^0.3.3",
35
- "@uniweb/core": "^0.8.5"
34
+ "@uniweb/core": "^0.10.0",
35
+ "@uniweb/content-writer": "^0.3.3"
36
36
  },
37
37
  "devDependencies": {
38
38
  "vitest": "^4.1.7",
@@ -6,26 +6,57 @@
6
6
  * per-record detail files into each item before calling this function.
7
7
  */
8
8
 
9
+ /**
10
+ * Compose a record's route the way the build does, for the sources that have
11
+ * not already done it.
12
+ *
13
+ * `collections[name].route` is authored in `site.yml` (see the blog and
14
+ * international templates). When it is set, the build's collection processor
15
+ * already stamps `item.route` on every record — so the route below is a
16
+ * *fallback* for records that arrived without one (an API-backed collection a
17
+ * host assembled itself), never a second opinion about records that have one.
18
+ *
19
+ * The trailing-slash strip matches `collectItems` in
20
+ * `@uniweb/build`'s `site/collection-processor.js`. Without it a `route:
21
+ * /blog/` authored with a slash yields `/blog//my-post` here and
22
+ * `/blog/my-post` there — two answers to one question, on a value nobody
23
+ * checks until a visitor clicks it.
24
+ */
25
+ function composeRoute(configRoute, slug) {
26
+ if (typeof configRoute !== 'string' || configRoute === '') return undefined
27
+ return `${configRoute.replace(/\/$/, '')}/${slug}`
28
+ }
29
+
9
30
  /**
10
31
  * @param {string} name - Collection name (e.g. "articles")
11
32
  * @param {Object} config - Collection config from site.yml (config.collections[name])
12
- * @param {Object} collectionData - Parsed cascade JSON (`data/{name}.json`)
33
+ * @param {Object[]|Object} collectionData - Parsed cascade JSON (`data/{name}.json`),
34
+ * which the build writes as a bare array. The `{ items: [...] }` envelope is
35
+ * accepted too, since a host fetching the collection from a backend may carry one.
13
36
  * @param {string} locale - Locale code (e.g. "en")
14
37
  * @returns {Object} Collection search index
15
38
  */
16
39
  export function generateCollectionIndex(name, config, collectionData, locale) {
17
40
  const fields = config.search?.fields || ['title']
18
41
  const weight = config.search?.weight ?? 0.7
19
- const items = collectionData?.items || []
42
+ const items = Array.isArray(collectionData)
43
+ ? collectionData
44
+ : collectionData?.items || []
20
45
 
21
46
  const entries = items.map(item => {
22
47
  const content = fields.map(f => item[f] || '').filter(Boolean).join(' ')
23
48
  const slug = item.slug || item.id || String(item.title || '').toLowerCase().replace(/\s+/g, '-')
49
+ // The record's own route wins: the build already resolved it against the
50
+ // same config, so recomputing here could only disagree. `route` is omitted
51
+ // entirely when neither source can supply one — a missing key is detectable
52
+ // by a consumer, where the string "undefined/my-post" is a link that ranks
53
+ // correctly, looks plausible, and 404s on click.
54
+ const route = item.route || composeRoute(config.route, slug)
24
55
  return {
25
56
  id: `collection:${name}:${slug}`,
26
57
  type: 'collection',
27
58
  collection: name,
28
- route: `${config.route}/${slug}`,
59
+ ...(route ? { route } : {}),
29
60
  title: item.title || item.name || slug,
30
61
  content,
31
62
  excerpt: content.length > 160