@uniweb/projections 0.3.0 → 0.3.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 +3 -3
- package/src/config.js +23 -0
- package/src/index.js +1 -0
- package/src/pages.js +18 -7
- package/src/search/collections.js +34 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/projections",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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/
|
|
35
|
-
"@uniweb/
|
|
34
|
+
"@uniweb/core": "^0.9.0",
|
|
35
|
+
"@uniweb/content-writer": "^0.3.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"vitest": "^4.1.7",
|
package/src/config.js
CHANGED
|
@@ -21,6 +21,29 @@ const DEFAULTS = {
|
|
|
21
21
|
branchMinPages: DEFAULT_BRANCH_MIN_PAGES,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Every key the `agents:` block accepts — the author-facing vocabulary.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ **This is deliberately WIDER than what {@link resolveAgentsConfig} reads.**
|
|
28
|
+
* Some keys are *carried* rather than honoured: the framework passes them
|
|
29
|
+
* through to the host, which enforces them, and this package never looks at
|
|
30
|
+
* them. `expectedOrigins` is the first — the host checks the `Origin` of
|
|
31
|
+
* requests to its agent endpoint against it.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ **A carry-only key still has to be listed here, and the reason is the whole
|
|
34
|
+
* point of the list.** The block reaches a backend as opaque JSON, so nothing
|
|
35
|
+
* downstream can reject a typo — an author who writes `expectedOrgins` gets a
|
|
36
|
+
* site that looks configured and checks nothing, silently, forever. The only
|
|
37
|
+
* lane that can catch it is the one that owns the words. `uniweb doctor` reads
|
|
38
|
+
* this list; if you add a key to the block and not to this list, doctor will
|
|
39
|
+
* call the author's correct spelling a typo.
|
|
40
|
+
*/
|
|
41
|
+
export const AGENTS_KEYS = Object.freeze([
|
|
42
|
+
...Object.keys(DEFAULTS),
|
|
43
|
+
// Carried, not honoured here — see the note above before removing.
|
|
44
|
+
'expectedOrigins',
|
|
45
|
+
])
|
|
46
|
+
|
|
24
47
|
/**
|
|
25
48
|
* Read the site's `agents:` block.
|
|
26
49
|
*
|
package/src/index.js
CHANGED
package/src/pages.js
CHANGED
|
@@ -109,13 +109,24 @@ export function isKnowledgeRoute(route, roots) {
|
|
|
109
109
|
* rendered and cannot be reached by a visitor, so nothing describing the
|
|
110
110
|
* *public* site may name it.
|
|
111
111
|
*
|
|
112
|
-
* ⛔ That last one is
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
112
|
+
* ⛔ **That last one is about who the prose is ADDRESSED to — it is not a
|
|
113
|
+
* confidentiality boundary, and reading it as one produces wrong designs.**
|
|
114
|
+
* A knowledge page is source material for a service the site runs for its
|
|
115
|
+
* visitors; the explanations in it are written for that service to reason
|
|
116
|
+
* with, not for a person or a crawler to read. So naming it in `llms.txt`,
|
|
117
|
+
* `/{route}.md` or the search index is not "exposing a secret" — it is
|
|
118
|
+
* serving a reader prose that was written for somebody else, in a file that
|
|
119
|
+
* claims to describe the public site.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ **Do not build a security expectation on this.** The service can quote
|
|
122
|
+
* its source material back to whoever prompts it — that is what it is for —
|
|
123
|
+
* so knowledge content is reachable by a visitor through the service by
|
|
124
|
+
* design. [Diego, 2026-08-13]: *"It is not the case that it's private in the
|
|
125
|
+
* sense of sensitive. It is given to the agent so they can reason and respond
|
|
126
|
+
* prompts."*
|
|
127
|
+
*
|
|
128
|
+
* The omission still mattered, just not for the reason first written here: it
|
|
129
|
+
* also made {@link selectCorpusPages} degenerate. That selector is *public ∪
|
|
119
130
|
* knowledge*, and while knowledge rode in the public half the union added
|
|
120
131
|
* nothing and read as if it worked.
|
|
121
132
|
*
|
|
@@ -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
|
|
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
|
|
59
|
+
...(route ? { route } : {}),
|
|
29
60
|
title: item.title || item.name || slug,
|
|
30
61
|
content,
|
|
31
62
|
excerpt: content.length > 160
|