@uniweb/projections 0.1.3 → 0.1.5

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.1.3",
3
+ "version": "0.1.5",
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,7 +31,7 @@
31
31
  "node": ">=20.19"
32
32
  },
33
33
  "dependencies": {
34
- "@uniweb/core": "0.7.32",
34
+ "@uniweb/core": "0.7.33",
35
35
  "@uniweb/content-writer": "0.2.9"
36
36
  },
37
37
  "devDependencies": {
@@ -108,12 +108,17 @@ function extractFromPage(page) {
108
108
 
109
109
  /**
110
110
  * Extract search entries from a section (and subsections)
111
+ *
111
112
  * @param {Object} section - Section data
112
113
  * @param {Object} page - Parent page
113
114
  * @param {Object} options - Extraction options
115
+ * @param {string} [ancestorAnchor] - The anchor of the nearest ancestor that
116
+ * is actually rendered with a wrapper. Absent at the top level, where the
117
+ * section renders itself; inherited by every nested level. See the note on
118
+ * anchors below.
114
119
  * @returns {Array<Object>} Array of search entries
115
120
  */
116
- function extractFromSection(section, page, options) {
121
+ function extractFromSection(section, page, options, ancestorAnchor) {
117
122
  const entries = []
118
123
  const {
119
124
  includeHeadings,
@@ -126,6 +131,27 @@ function extractFromSection(section, page, options) {
126
131
  const sectionId = section.id || 'unknown'
127
132
  const component = section.component || section.type || 'unknown'
128
133
 
134
+ // Where a reader gets taken for a hit in this section.
135
+ //
136
+ // A top-level section is wrapped by the renderer and carries its own id, so
137
+ // it is its own destination. A NESTED section usually is not: `<ChildBlocks>`
138
+ // renders children bare by default — no wrapper, and therefore no id — and
139
+ // whether a foundation opted into `wrapAs` is a runtime decision this
140
+ // build-time pass cannot see. Emitting the child's own id produced a
141
+ // fragment that existed nowhere, so a hit landed on the page without
142
+ // scrolling and did nothing at all when the reader was already on it.
143
+ //
144
+ // The nearest rendered ancestor is the honest answer: it always exists, and
145
+ // the child's content is physically inside it, so the reader lands where the
146
+ // matching text actually is. Precision is lost only in the rarer `wrapAs`
147
+ // case, where the alternative was being wrong in the common one.
148
+ //
149
+ // Consequence worth knowing: a parent and its children now share an anchor,
150
+ // so several results can point at one destination. They are still distinct
151
+ // content with their own titles and excerpts; which to surface is the
152
+ // consumer's ranking decision, not ours to collapse here.
153
+ const anchor = ancestorAnchor || sectionDomId(section)
154
+
129
155
  // Skip excluded components
130
156
  if (excludeComponents.includes(component)) {
131
157
  return entries
@@ -162,12 +188,9 @@ function extractFromSection(section, page, options) {
162
188
  type: 'section',
163
189
  route: page.route,
164
190
  sectionId,
165
- // The anchor MUST equal the id the renderer writes, or the result links
166
- // to a fragment that does not exist it lands on the page without
167
- // scrolling, and does nothing at all when the target is the current
168
- // page. Derived by the shared rule rather than spelled out here,
169
- // because that is exactly how the two drifted apart before.
170
- anchor: sectionDomId(section),
191
+ // Resolved above. Never spelled out here: the id format is the shared
192
+ // rule's to own, which is exactly how these two drifted apart before.
193
+ anchor,
171
194
  component,
172
195
  title: sectionTitle,
173
196
  pageTitle: page.title || '',
@@ -179,9 +202,14 @@ function extractFromSection(section, page, options) {
179
202
  })
180
203
  }
181
204
 
182
- // Recursively process subsections
205
+ // Recursively process subsections.
206
+ //
207
+ // `anchor` is passed down rather than the subsection resolving its own, so
208
+ // every level below the rendered one inherits the same destination — a
209
+ // grandchild is no more rendered than its parent, and inheriting only one
210
+ // level would put it back to naming an id that does not exist.
183
211
  for (const subsection of section.subsections || []) {
184
- const subEntries = extractFromSection(subsection, page, options)
212
+ const subEntries = extractFromSection(subsection, page, options, anchor)
185
213
  entries.push(...subEntries)
186
214
  }
187
215