@redpanda-data/docs-extensions-and-macros 4.17.3 → 5.0.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/extension-utils/llms-utils.js +2 -5
- package/extensions/algolia-indexer/generate-index.js +99 -54
- package/extensions/collect-bloblang-samples.js +6 -6
- package/extensions/compute-end-of-life.js +8 -4
- package/extensions/conditional-home-redirect.js +90 -0
- package/extensions/convert-to-markdown.js +23 -1
- package/extensions/find-related-docs.js +3 -3
- package/extensions/find-related-labs.js +1 -1
- package/extensions/generate-fields-only-pages.js +7 -7
- package/extensions/generate-rp-connect-categories.js +5 -5
- package/extensions/generate-rp-connect-info.js +6 -6
- package/extensions/modify-connect-tag-playbook.js +1 -1
- package/extensions/unified-navigation.js +669 -0
- package/extensions/unlisted-pages.js +5 -1
- package/extensions/unpublish-pages.js +12 -0
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +7 -2
- package/extensions/version-fetcher/get-latest-connect.js +6 -2
- package/extensions/version-fetcher/get-latest-console-version.js +13 -4
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +31 -5
- package/extensions/version-fetcher/get-latest-redpanda-version.js +8 -3
- package/extensions/version-fetcher/retry-util.js +10 -3
- package/extensions/version-fetcher/set-latest-version.js +9 -8
- package/macros/rp-connect-components.js +62 -1
- package/package.json +3 -1
- package/tools/redpanda-connect/helpers/renderConnectFields.js +1 -1
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +133 -83
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const yaml = require('js-yaml')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unified Navigation Extension (Config-Driven)
|
|
7
|
+
*
|
|
8
|
+
* Creates a unified navigation structure based on component configuration.
|
|
9
|
+
* Components define their navigation hierarchy via the 'page-navigation' attribute in antora.yml.
|
|
10
|
+
* Supports nested buckets with version detection and breadcrumb hierarchy computation.
|
|
11
|
+
*
|
|
12
|
+
* Configuration: No playbook configuration needed - reads from component antora.yml files.
|
|
13
|
+
*
|
|
14
|
+
* Component antora.yml configuration:
|
|
15
|
+
* asciidoc:
|
|
16
|
+
* attributes:
|
|
17
|
+
* component-metadata:
|
|
18
|
+
* title: "Component Name"
|
|
19
|
+
* color: "#hexcolor"
|
|
20
|
+
* icon: "tabler-icon-name"
|
|
21
|
+
* order: 10
|
|
22
|
+
* page-navigation:
|
|
23
|
+
* - component-name
|
|
24
|
+
* - parent-component:
|
|
25
|
+
* - child-component-1
|
|
26
|
+
* - child-component-2
|
|
27
|
+
*
|
|
28
|
+
* Output: Attaches to pages with page-navigation config:
|
|
29
|
+
* - 'page-custom-navigation' (JSON array of buckets)
|
|
30
|
+
* - 'page-has-custom-nav' (boolean)
|
|
31
|
+
* - 'page-is-umbrella-nav' (boolean)
|
|
32
|
+
* - 'page-breadcrumb-hierarchy' (JSON array of breadcrumb items)
|
|
33
|
+
*
|
|
34
|
+
* Components without page-navigation use standard Antora navigation.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
module.exports.register = function () {
|
|
38
|
+
const logger = this.getLogger('unified-navigation-extension')
|
|
39
|
+
|
|
40
|
+
this.on('navigationBuilt', ({ contentCatalog }) => {
|
|
41
|
+
try {
|
|
42
|
+
// Build a set of published page URLs (pages that have page.out defined)
|
|
43
|
+
const allPages = contentCatalog.getPages()
|
|
44
|
+
const publishedUrlsSet = new Set(
|
|
45
|
+
allPages
|
|
46
|
+
.filter((page) => page.out && page.pub && page.pub.url)
|
|
47
|
+
.map((page) => page.pub.url)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const components = contentCatalog.getComponents()
|
|
51
|
+
|
|
52
|
+
// Build component version navigation map (used by both config-driven and section-based nav)
|
|
53
|
+
const componentVersionNavMap = buildComponentVersionNavMap(components)
|
|
54
|
+
|
|
55
|
+
// PHASE 1: Config-Driven Navigation
|
|
56
|
+
// Step 1: Collect all components with page-navigation config
|
|
57
|
+
const pages = contentCatalog.getPages()
|
|
58
|
+
const configDrivenPages = new Set()
|
|
59
|
+
const componentConfigs = new Map() // component -> {configTree, configOwner}
|
|
60
|
+
|
|
61
|
+
// First pass: find components with page-navigation config
|
|
62
|
+
for (const component of components) {
|
|
63
|
+
for (const version of component.versions) {
|
|
64
|
+
const navConfig = version.asciidoc?.attributes?.['page-navigation']
|
|
65
|
+
if (navConfig) {
|
|
66
|
+
try {
|
|
67
|
+
const configTree = parseNavigationConfig(navConfig)
|
|
68
|
+
if (configTree.length > 0) {
|
|
69
|
+
// Store config and extract all component names mentioned
|
|
70
|
+
const allComponentsInConfig = new Set()
|
|
71
|
+
const extractComponents = (tree) => {
|
|
72
|
+
for (const item of tree) {
|
|
73
|
+
allComponentsInConfig.add(item.name)
|
|
74
|
+
if (item.children) {
|
|
75
|
+
extractComponents(item.children)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
extractComponents(configTree)
|
|
80
|
+
|
|
81
|
+
// Store config for the owner
|
|
82
|
+
// A component's OWN config always takes priority (regardless of depth)
|
|
83
|
+
const existingOwnerConfig = componentConfigs.get(component.name)
|
|
84
|
+
|
|
85
|
+
if (!existingOwnerConfig || existingOwnerConfig.configOwner !== component.name) {
|
|
86
|
+
// No existing config, or existing config is from another component - use this one
|
|
87
|
+
componentConfigs.set(component.name, {
|
|
88
|
+
configTree,
|
|
89
|
+
configOwner: component.name,
|
|
90
|
+
allComponents: allComponentsInConfig,
|
|
91
|
+
})
|
|
92
|
+
} else {
|
|
93
|
+
// Existing config is also from this component - compare depths
|
|
94
|
+
const existingOwnerDepth = getComponentDepth(existingOwnerConfig.configTree, component.name)
|
|
95
|
+
const newOwnerDepth = getComponentDepth(configTree, component.name)
|
|
96
|
+
if (newOwnerDepth >= existingOwnerDepth) {
|
|
97
|
+
// New config has equal or better hierarchy, use it
|
|
98
|
+
componentConfigs.set(component.name, {
|
|
99
|
+
configTree,
|
|
100
|
+
configOwner: component.name,
|
|
101
|
+
allComponents: allComponentsInConfig,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Also store this config for all child components
|
|
107
|
+
// Prefer configs with deeper nesting (more parent context for breadcrumbs)
|
|
108
|
+
for (const childComponent of allComponentsInConfig) {
|
|
109
|
+
const existingConfig = componentConfigs.get(childComponent)
|
|
110
|
+
const newDepth = getComponentDepth(configTree, childComponent)
|
|
111
|
+
|
|
112
|
+
if (!existingConfig) {
|
|
113
|
+
// No existing config, store this one
|
|
114
|
+
componentConfigs.set(childComponent, {
|
|
115
|
+
configTree,
|
|
116
|
+
configOwner: component.name,
|
|
117
|
+
allComponents: allComponentsInConfig,
|
|
118
|
+
})
|
|
119
|
+
} else {
|
|
120
|
+
// Compare depths - prefer deeper nesting (more parent context)
|
|
121
|
+
const existingDepth = getComponentDepth(existingConfig.configTree, childComponent)
|
|
122
|
+
if (newDepth > existingDepth) {
|
|
123
|
+
// New config has more parent context, prefer it for breadcrumbs
|
|
124
|
+
componentConfigs.set(childComponent, {
|
|
125
|
+
configTree,
|
|
126
|
+
configOwner: component.name,
|
|
127
|
+
allComponents: allComponentsInConfig,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error(`Error parsing page-navigation for ${component.name}: ${error.message}`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Step 1.5: Find the home component's config for breadcrumb calculations
|
|
141
|
+
// Home has the full hierarchy, so use it for ALL breadcrumb computations
|
|
142
|
+
const homeConfig = componentConfigs.get('home')
|
|
143
|
+
|
|
144
|
+
// Step 2: Apply navigation to all pages of components mentioned in configs
|
|
145
|
+
for (const page of pages) {
|
|
146
|
+
if (!page.src) continue
|
|
147
|
+
|
|
148
|
+
const configData = componentConfigs.get(page.src.component)
|
|
149
|
+
if (!configData) continue // No config for this component
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Filter config tree to show only relevant hierarchy for current page
|
|
153
|
+
const relevantTree = filterConfigTreeForComponent(configData.configTree, page.src.component)
|
|
154
|
+
|
|
155
|
+
// Check if this component has children and owns the config
|
|
156
|
+
const hasChildComponents = componentHasChildren(configData.configTree, page.src.component)
|
|
157
|
+
const ownsConfig = configData.configOwner === page.src.component
|
|
158
|
+
|
|
159
|
+
// If component has children and owns config, we'll show its nav items outside buckets
|
|
160
|
+
// So build buckets only for children, not for the parent itself
|
|
161
|
+
let treeForBuckets = relevantTree
|
|
162
|
+
if (hasChildComponents && ownsConfig) {
|
|
163
|
+
// Extract children from ORIGINAL configTree (not filtered relevantTree)
|
|
164
|
+
// because filterConfigTreeForComponent sets children: undefined on parent
|
|
165
|
+
const parentNode = findNodeInTree(configData.configTree, page.src.component)
|
|
166
|
+
if (parentNode && parentNode.children) {
|
|
167
|
+
treeForBuckets = parentNode.children
|
|
168
|
+
} else {
|
|
169
|
+
logger.warn(`${page.src.component}: Has children but couldn't find them in configTree`)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const buckets = buildBucketsFromConfig(
|
|
174
|
+
treeForBuckets,
|
|
175
|
+
contentCatalog,
|
|
176
|
+
page.src.component,
|
|
177
|
+
page.src.version,
|
|
178
|
+
publishedUrlsSet,
|
|
179
|
+
componentVersionNavMap,
|
|
180
|
+
page
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
// Always compute breadcrumb hierarchy from HOME config (which has full hierarchy)
|
|
184
|
+
// This ensures components like self-managed show "Docs > Data Platform > Self-Managed"
|
|
185
|
+
// instead of just "Docs > Self-Managed"
|
|
186
|
+
page.asciidoc = page.asciidoc || {}
|
|
187
|
+
page.asciidoc.attributes = page.asciidoc.attributes || {}
|
|
188
|
+
|
|
189
|
+
if (homeConfig) {
|
|
190
|
+
const hierarchy = findComponentPath(homeConfig.configTree, page.src.component, contentCatalog)
|
|
191
|
+
if (hierarchy) {
|
|
192
|
+
page.asciidoc.attributes['page-breadcrumb-hierarchy'] = JSON.stringify(hierarchy)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Only set custom navigation if buckets exist AND component is not standalone
|
|
197
|
+
// Standalone components appear at root level with no children (e.g., agentic-data-plane)
|
|
198
|
+
// They appear in product switcher/home nav but use standard Antora nav on their own pages
|
|
199
|
+
const isStandaloneComponent = isStandalone(configData.configTree, page.src.component)
|
|
200
|
+
|
|
201
|
+
if (buckets.length > 0 && !isStandaloneComponent) {
|
|
202
|
+
// Build full navigation array
|
|
203
|
+
let fullNavigation = []
|
|
204
|
+
|
|
205
|
+
// If component has children and owns config, show its nav items outside buckets
|
|
206
|
+
if (hasChildComponents && ownsConfig) {
|
|
207
|
+
// Get the component's own nav items (from nav.adoc) to prepend before child buckets
|
|
208
|
+
const compData = componentVersionNavMap.get(page.src.component)
|
|
209
|
+
let ownNavItems = []
|
|
210
|
+
if (compData) {
|
|
211
|
+
const versionData = compData.versionMap.get(page.src.version)
|
|
212
|
+
if (versionData && versionData.navigation) {
|
|
213
|
+
ownNavItems = filterUnpublishedPages(versionData.navigation, publishedUrlsSet)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// If parent component has its own nav items, wrap them in a pseudo-bucket with showNavItemsOnly
|
|
218
|
+
// This renders the items directly without bucket header/wrapper
|
|
219
|
+
if (ownNavItems.length > 0) {
|
|
220
|
+
fullNavigation.push({
|
|
221
|
+
items: ownNavItems,
|
|
222
|
+
showNavItemsOnly: true,
|
|
223
|
+
isCurrentBucket: true,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Add child component buckets
|
|
229
|
+
fullNavigation = fullNavigation.concat(buckets)
|
|
230
|
+
|
|
231
|
+
page.asciidoc.attributes['page-custom-navigation'] = JSON.stringify(fullNavigation)
|
|
232
|
+
page.asciidoc.attributes['page-has-custom-nav'] = 'true'
|
|
233
|
+
// Set is-umbrella-nav to show parent bucket headers
|
|
234
|
+
page.asciidoc.attributes['page-is-umbrella-nav'] = 'true'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
configDrivenPages.add(page.src.component)
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error(`Error processing navigation for ${page.src.path}: ${error.message}`)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (configDrivenPages.size > 0) {
|
|
244
|
+
logger.info(`Processed config-driven navigation for ${configDrivenPages.size} components: ${Array.from(configDrivenPages).join(', ')}`)
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error(`Error building unified navigation: ${error.message}`)
|
|
248
|
+
logger.error(error.stack)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build a map of component navigation data for quick lookup
|
|
255
|
+
* @param {Array} components - Array of component objects from content catalog
|
|
256
|
+
* @returns {Map} Map of component name -> { component, versionMap, latestVersion }
|
|
257
|
+
*/
|
|
258
|
+
function buildComponentVersionNavMap(components) {
|
|
259
|
+
const map = new Map()
|
|
260
|
+
|
|
261
|
+
for (const component of components) {
|
|
262
|
+
const versionMap = new Map()
|
|
263
|
+
for (const version of component.versions) {
|
|
264
|
+
versionMap.set(version.version, {
|
|
265
|
+
navigation: version.navigation || [],
|
|
266
|
+
displayVersion: version.displayVersion || version.version,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
map.set(component.name, {
|
|
271
|
+
component,
|
|
272
|
+
versionMap,
|
|
273
|
+
latestVersion: component.latestVersion || component.versions[0],
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return map
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Filter navigation items to exclude unpublished pages
|
|
282
|
+
* @param {Array} items - Navigation items array
|
|
283
|
+
* @param {Map} publishedUrlsSet - Set of published page URLs
|
|
284
|
+
* @returns {Array} Filtered navigation items
|
|
285
|
+
*/
|
|
286
|
+
function filterUnpublishedPages(items, publishedUrlsSet) {
|
|
287
|
+
if (!Array.isArray(items)) return []
|
|
288
|
+
|
|
289
|
+
return items
|
|
290
|
+
.map((item) => {
|
|
291
|
+
// If item has a URL, check if it's in the published URLs set
|
|
292
|
+
if (item.url) {
|
|
293
|
+
// Skip items whose pages aren't published
|
|
294
|
+
if (!publishedUrlsSet.has(item.url)) {
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Return a new object with filtered children to avoid mutating the original
|
|
300
|
+
if (item.items && Array.isArray(item.items)) {
|
|
301
|
+
return { ...item, items: filterUnpublishedPages(item.items, publishedUrlsSet) }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { ...item }
|
|
305
|
+
})
|
|
306
|
+
.filter(Boolean) // Remove null entries
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Extract component-metadata from a component version
|
|
311
|
+
* @param {Object} version - Component version object
|
|
312
|
+
* @returns {Object|null} The component-metadata object or null
|
|
313
|
+
*/
|
|
314
|
+
function getHeaderData(version) {
|
|
315
|
+
if (!version || !version.asciidoc || !version.asciidoc.attributes) {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
return version.asciidoc.attributes['component-metadata'] || null
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Find a node in the config tree by name
|
|
323
|
+
* @param {Array} tree - Navigation config tree
|
|
324
|
+
* @param {string} componentName - Component name to search for
|
|
325
|
+
* @returns {Object|null} The node or null if not found
|
|
326
|
+
*/
|
|
327
|
+
function findNodeInTree(tree, componentName) {
|
|
328
|
+
if (!Array.isArray(tree)) return null
|
|
329
|
+
|
|
330
|
+
for (const item of tree) {
|
|
331
|
+
if (item.name === componentName) {
|
|
332
|
+
return item
|
|
333
|
+
}
|
|
334
|
+
if (item.children) {
|
|
335
|
+
const found = findNodeInTree(item.children, componentName)
|
|
336
|
+
if (found) return found
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if a component has children in the config tree
|
|
344
|
+
* @param {Array} tree - Navigation config tree
|
|
345
|
+
* @param {string} componentName - Component name to search for
|
|
346
|
+
* @returns {boolean} True if component has children
|
|
347
|
+
*/
|
|
348
|
+
function componentHasChildren(tree, componentName) {
|
|
349
|
+
const node = findNodeInTree(tree, componentName)
|
|
350
|
+
return !!(node && node.children && node.children.length > 0)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Find the depth of a component in a config tree
|
|
355
|
+
* @param {Array} tree - Navigation config tree
|
|
356
|
+
* @param {string} targetComponent - Component name to find
|
|
357
|
+
* @param {number} currentDepth - Current depth in recursion
|
|
358
|
+
* @returns {number} Depth of component (0 = root level), or -1 if not found
|
|
359
|
+
*/
|
|
360
|
+
function getComponentDepth(tree, targetComponent, currentDepth = 0) {
|
|
361
|
+
for (const item of tree) {
|
|
362
|
+
if (item.name === targetComponent) {
|
|
363
|
+
return currentDepth
|
|
364
|
+
}
|
|
365
|
+
if (item.children) {
|
|
366
|
+
const childDepth = getComponentDepth(item.children, targetComponent, currentDepth + 1)
|
|
367
|
+
if (childDepth !== -1) return childDepth
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return -1 // Not found
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Parse page-navigation YAML config into component tree structure
|
|
375
|
+
* @param {string|array} navConfig - YAML string or parsed array from page-navigation attribute
|
|
376
|
+
* @returns {Array} Array of bucket definitions with hierarchy
|
|
377
|
+
*
|
|
378
|
+
* Example input:
|
|
379
|
+
* - data-platform:
|
|
380
|
+
* - cloud-data-platform
|
|
381
|
+
* - self-managed: [streaming, connect]
|
|
382
|
+
* - redpanda-adp
|
|
383
|
+
*
|
|
384
|
+
* Example output:
|
|
385
|
+
* [{
|
|
386
|
+
* name: 'data-platform',
|
|
387
|
+
* children: [
|
|
388
|
+
* { name: 'cloud-data-platform' },
|
|
389
|
+
* { name: 'self-managed', children: [{ name: 'streaming' }, { name: 'connect' }] }
|
|
390
|
+
* ]
|
|
391
|
+
* }, { name: 'redpanda-adp' }]
|
|
392
|
+
*/
|
|
393
|
+
function parseNavigationConfig(navConfig) {
|
|
394
|
+
// If it's already an array (from YAML parsing), use it directly
|
|
395
|
+
let config
|
|
396
|
+
if (typeof navConfig === 'string') {
|
|
397
|
+
try {
|
|
398
|
+
config = yaml.load(navConfig)
|
|
399
|
+
} catch (error) {
|
|
400
|
+
// Log YAML parsing error with context
|
|
401
|
+
console.error(`Failed to parse page-navigation YAML: ${error.message}`)
|
|
402
|
+
console.error(`YAML content: ${navConfig}`)
|
|
403
|
+
return []
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
config = navConfig
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!Array.isArray(config)) {
|
|
410
|
+
return []
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseItem(item) {
|
|
414
|
+
if (typeof item === 'string') {
|
|
415
|
+
// Simple component name
|
|
416
|
+
return { name: item }
|
|
417
|
+
} else if (typeof item === 'object' && item !== null) {
|
|
418
|
+
// Object with component name as key and children as value
|
|
419
|
+
const keys = Object.keys(item)
|
|
420
|
+
if (keys.length === 0) return null
|
|
421
|
+
|
|
422
|
+
const name = keys[0]
|
|
423
|
+
const childrenValue = item[name]
|
|
424
|
+
|
|
425
|
+
if (!childrenValue) {
|
|
426
|
+
return { name }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Parse children (can be array of strings, objects, or mix)
|
|
430
|
+
const children = Array.isArray(childrenValue)
|
|
431
|
+
? childrenValue.map(parseItem).filter(Boolean)
|
|
432
|
+
: [parseItem(childrenValue)].filter(Boolean)
|
|
433
|
+
|
|
434
|
+
return { name, children: children.length > 0 ? children : undefined }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return config.map(parseItem).filter(Boolean)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Build navigation buckets from parsed config tree
|
|
445
|
+
* @param {Array} configTree - Parsed navigation config tree
|
|
446
|
+
* @param {Object} contentCatalog - Antora content catalog
|
|
447
|
+
* @param {string} currentPageComponent - Current page's component name
|
|
448
|
+
* @param {string} currentPageVersion - Current page's version
|
|
449
|
+
* @param {Set} publishedUrlsSet - Set of published page URLs
|
|
450
|
+
* @param {Map} componentVersionNavMap - Map of component navigation data
|
|
451
|
+
* @returns {Array} Array of bucket objects for templates
|
|
452
|
+
*/
|
|
453
|
+
/**
|
|
454
|
+
* Check if a component is standalone (root-level with no children)
|
|
455
|
+
* Standalone components appear in product switcher but use standard Antora nav
|
|
456
|
+
* @param {Array} configTree - Parsed navigation config tree
|
|
457
|
+
* @param {string} componentName - Component name to check
|
|
458
|
+
* @returns {boolean} True if component is standalone
|
|
459
|
+
*/
|
|
460
|
+
function isStandalone(configTree, componentName) {
|
|
461
|
+
for (const item of configTree) {
|
|
462
|
+
if (item.name === componentName) {
|
|
463
|
+
// Component found at root level
|
|
464
|
+
return !item.children || item.children.length === 0
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Filter config tree to show only relevant hierarchy for a component
|
|
472
|
+
* - If component is at root level: return entire tree
|
|
473
|
+
* - If component is a parent (has children): return that parent with its children
|
|
474
|
+
* - If component is a child (leaf): return parent + siblings (parent marked for nav-only display)
|
|
475
|
+
* @param {Array} configTree - Full navigation config tree
|
|
476
|
+
* @param {string} componentName - Current page component
|
|
477
|
+
* @returns {Array} Filtered config tree
|
|
478
|
+
*/
|
|
479
|
+
function filterConfigTreeForComponent(configTree, componentName) {
|
|
480
|
+
// Check if component is at root level
|
|
481
|
+
for (const item of configTree) {
|
|
482
|
+
if (item.name === componentName) {
|
|
483
|
+
// Component is at root - if it has children, show parent nav items + children as buckets
|
|
484
|
+
if (item.children) {
|
|
485
|
+
// Return parent with showNavItemsOnly flag + children as separate buckets
|
|
486
|
+
return [{ ...item, showNavItemsOnly: true, children: undefined }, ...item.children]
|
|
487
|
+
}
|
|
488
|
+
return configTree
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Component is nested - find it and determine if it's a parent or child
|
|
493
|
+
function findComponentAndParent(items, parent = null) {
|
|
494
|
+
for (const item of items) {
|
|
495
|
+
if (item.name === componentName) {
|
|
496
|
+
// Found the component
|
|
497
|
+
if (item.children) {
|
|
498
|
+
// Component is a parent - return it with its children
|
|
499
|
+
return { isParent: true, parent: null, result: [item] }
|
|
500
|
+
} else {
|
|
501
|
+
// Component is a leaf child
|
|
502
|
+
// Check if there are other leaf siblings (to determine if we should show siblings)
|
|
503
|
+
const siblings = parent ? parent.children.filter(child => child.name !== componentName) : []
|
|
504
|
+
const hasLeafSiblings = siblings.some(sibling => !sibling.children)
|
|
505
|
+
|
|
506
|
+
if (hasLeafSiblings) {
|
|
507
|
+
// Has other leaf siblings - show parent nav + all siblings (like Streaming/Connect)
|
|
508
|
+
// Mark parent as "showNavItemsOnly" so it doesn't render as a bucket header
|
|
509
|
+
return { isParent: false, parent: parent, result: parent ? [{ ...parent, showNavItemsOnly: true }, ...parent.children] : [item] }
|
|
510
|
+
} else {
|
|
511
|
+
// No leaf siblings (standalone like Cloud) - return empty to use standard nav
|
|
512
|
+
return { isParent: false, parent: null, result: [] }
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (item.children) {
|
|
518
|
+
const found = findComponentAndParent(item.children, item)
|
|
519
|
+
if (found) return found
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const found = findComponentAndParent(configTree)
|
|
526
|
+
return found ? found.result : configTree // Fallback to full tree if not found
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function buildBucketsFromConfig(
|
|
530
|
+
configTree,
|
|
531
|
+
contentCatalog,
|
|
532
|
+
currentPageComponent,
|
|
533
|
+
currentPageVersion,
|
|
534
|
+
publishedUrlsSet,
|
|
535
|
+
componentVersionNavMap,
|
|
536
|
+
page
|
|
537
|
+
) {
|
|
538
|
+
// Get list of buckets to expand by default from page attribute
|
|
539
|
+
const expandBucketsAttr = page?.asciidoc?.attributes?.['page-expand-buckets'] || ''
|
|
540
|
+
const expandBuckets = expandBucketsAttr.split(',').map((s) => s.trim()).filter(Boolean)
|
|
541
|
+
|
|
542
|
+
function buildBucket(configItem) {
|
|
543
|
+
const componentName = configItem.name
|
|
544
|
+
const component = contentCatalog.getComponent(componentName)
|
|
545
|
+
|
|
546
|
+
if (!component) {
|
|
547
|
+
return null // Component not found
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const latestVersion = component.latestVersion || component.versions[0]
|
|
551
|
+
if (!latestVersion) {
|
|
552
|
+
return null
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Get metadata from component-metadata
|
|
556
|
+
const metadata = getHeaderData(latestVersion)
|
|
557
|
+
if (!metadata) {
|
|
558
|
+
return null
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Build versions array
|
|
562
|
+
const versions = component.versions.map((v) => ({
|
|
563
|
+
version: v.version,
|
|
564
|
+
displayVersion: v.displayVersion || v.version,
|
|
565
|
+
url: v.url,
|
|
566
|
+
isPrerelease: !!v.prerelease,
|
|
567
|
+
releaseDate: v.asciidoc?.attributes?.['page-release-date'] || null,
|
|
568
|
+
isEol: v.asciidoc?.attributes?.['page-is-past-eol'] === 'true',
|
|
569
|
+
}))
|
|
570
|
+
|
|
571
|
+
// Determine if this is the current bucket
|
|
572
|
+
const isCurrentBucket = componentName === currentPageComponent
|
|
573
|
+
|
|
574
|
+
// Check if this bucket should be expanded by default (from page attribute)
|
|
575
|
+
const isExpandedByDefault = expandBuckets.includes(componentName)
|
|
576
|
+
|
|
577
|
+
// Get navigation for this component
|
|
578
|
+
let navigation, displayVersion
|
|
579
|
+
const compData = componentVersionNavMap.get(componentName)
|
|
580
|
+
|
|
581
|
+
if (isCurrentBucket && compData) {
|
|
582
|
+
// For current bucket, use page's version
|
|
583
|
+
const versionData = compData.versionMap.get(currentPageVersion)
|
|
584
|
+
if (versionData) {
|
|
585
|
+
navigation = versionData.navigation
|
|
586
|
+
displayVersion = versionData.displayVersion
|
|
587
|
+
} else {
|
|
588
|
+
navigation = compData.latestVersion?.navigation || []
|
|
589
|
+
displayVersion = compData.latestVersion?.displayVersion || compData.latestVersion?.version
|
|
590
|
+
}
|
|
591
|
+
} else if (compData) {
|
|
592
|
+
// For other buckets, use latest version
|
|
593
|
+
navigation = compData.latestVersion?.navigation || []
|
|
594
|
+
displayVersion = compData.latestVersion?.displayVersion || compData.latestVersion?.version
|
|
595
|
+
} else {
|
|
596
|
+
navigation = []
|
|
597
|
+
displayVersion = latestVersion.displayVersion || latestVersion.version
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Filter unpublished pages
|
|
601
|
+
const filteredNavigation = filterUnpublishedPages(navigation, publishedUrlsSet)
|
|
602
|
+
|
|
603
|
+
const bucket = {
|
|
604
|
+
componentName,
|
|
605
|
+
title: metadata.title || component.title || componentName,
|
|
606
|
+
color: metadata.color || '#6366f1',
|
|
607
|
+
icon: metadata.icon || null,
|
|
608
|
+
order: metadata.order || 999,
|
|
609
|
+
versions,
|
|
610
|
+
currentVersion: displayVersion,
|
|
611
|
+
isCurrentBucket,
|
|
612
|
+
isExpandedByDefault,
|
|
613
|
+
items: filteredNavigation,
|
|
614
|
+
componentUrl: component.url,
|
|
615
|
+
showNavItemsOnly: configItem.showNavItemsOnly || false, // Flag for rendering nav items without bucket header,
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Process children recursively
|
|
619
|
+
if (configItem.children && configItem.children.length > 0) {
|
|
620
|
+
bucket.children = configItem.children.map(buildBucket).filter(Boolean)
|
|
621
|
+
// Determine if parent should be expanded
|
|
622
|
+
bucket.hasCurrentChild = bucket.children.some((c) => c.isCurrentBucket || c.hasCurrentChild)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return bucket
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return configTree.map(buildBucket).filter(Boolean)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Find component's position in navigation hierarchy for breadcrumbs
|
|
633
|
+
* @param {Array} configTree - Parsed navigation config tree
|
|
634
|
+
* @param {string} targetComponent - Component to find
|
|
635
|
+
* @param {Object} contentCatalog - Antora content catalog
|
|
636
|
+
* @returns {Array|null} Array of breadcrumb items or null
|
|
637
|
+
*/
|
|
638
|
+
function findComponentPath(configTree, targetComponent, contentCatalog) {
|
|
639
|
+
function search(items, path = []) {
|
|
640
|
+
for (const item of items) {
|
|
641
|
+
const componentName = item.name
|
|
642
|
+
const component = contentCatalog.getComponent(componentName)
|
|
643
|
+
|
|
644
|
+
if (!component) continue
|
|
645
|
+
|
|
646
|
+
const latestVersion = component.latestVersion || component.versions[0]
|
|
647
|
+
const metadata = latestVersion ? getHeaderData(latestVersion) : null
|
|
648
|
+
|
|
649
|
+
const breadcrumbItem = {
|
|
650
|
+
component: componentName,
|
|
651
|
+
title: metadata?.title || component.title || componentName,
|
|
652
|
+
url: component.url || `/${componentName}/`,
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (componentName === targetComponent) {
|
|
656
|
+
return [...path, breadcrumbItem]
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (item.children && item.children.length > 0) {
|
|
660
|
+
const childPath = search(item.children, [...path, breadcrumbItem])
|
|
661
|
+
if (childPath) return childPath
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return null
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return search(configTree)
|
|
669
|
+
}
|
|
@@ -7,7 +7,7 @@ module.exports.register = function ({ config }) {
|
|
|
7
7
|
this.on('navigationBuilt', ({ siteCatalog, contentCatalog }) => {
|
|
8
8
|
contentCatalog.getComponents().forEach(({ versions }) => {
|
|
9
9
|
versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
|
|
10
|
-
if (component === 'api' || component === '
|
|
10
|
+
if (component === 'api' || component === 'labs') return;
|
|
11
11
|
if (!nav) return;
|
|
12
12
|
|
|
13
13
|
const navEntriesByUrl = getNavEntriesByUrl(nav);
|
|
@@ -23,6 +23,10 @@ module.exports.register = function ({ config }) {
|
|
|
23
23
|
// Skip field-only pages (generated for includes, not standalone navigation)
|
|
24
24
|
if (page.isFieldOnlyPage) return collector;
|
|
25
25
|
|
|
26
|
+
// Skip technical pages that are intentionally unlisted (e.g., llms.txt, sitemap)
|
|
27
|
+
const technicalPages = ['llms', 'sitemap', 'sitemap-llms'];
|
|
28
|
+
if (technicalPages.includes(page.src.stem)) return collector;
|
|
29
|
+
|
|
26
30
|
if (!(page.pub.url in navEntriesByUrl) && page.pub.url !== defaultUrl) {
|
|
27
31
|
logger.warn({ file: page.src, source: page.src.origin }, 'detected unlisted page');
|
|
28
32
|
return collector.concat(page);
|