@fern-api/fdr-sdk 1.2.39-4ae10c3282 → 1.2.39-4e8e68c5b4
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/dist/js/client/FdrClient.js +1 -0
- package/dist/js/client/FdrClient.js.map +1 -1
- package/dist/js/client/FdrClient.mjs +1 -0
- package/dist/js/client/FdrClient.mjs.map +1 -1
- package/dist/js/converters/index.js +4 -9
- package/dist/js/converters/index.js.map +1 -1
- package/dist/js/converters/index.mjs +4 -9
- package/dist/js/converters/index.mjs.map +1 -1
- package/dist/js/index.js +5 -9
- package/dist/js/index.js.map +1 -1
- package/dist/js/index.mjs +5 -9
- package/dist/js/index.mjs.map +1 -1
- package/dist/js/navigation/index.js +4 -9
- package/dist/js/navigation/index.js.map +1 -1
- package/dist/js/navigation/index.mjs +4 -9
- package/dist/js/navigation/index.mjs.map +1 -1
- package/dist/js/navigation/ledger-root-builder.js +241 -63
- package/dist/js/navigation/ledger-root-builder.js.map +1 -1
- package/dist/js/navigation/ledger-root-builder.mjs +241 -63
- package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
- package/dist/js/orpc-client.js +1 -0
- package/dist/js/orpc-client.js.map +1 -1
- package/dist/js/orpc-client.mjs +1 -0
- package/dist/js/orpc-client.mjs.map +1 -1
- package/dist/navigation/__test__/findNode.authedRedirect.test.d.ts +2 -0
- package/dist/navigation/__test__/findNode.authedRedirect.test.d.ts.map +1 -0
- package/dist/navigation/__test__/findNode.authedRedirect.test.js +184 -0
- package/dist/navigation/__test__/findNode.authedRedirect.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js +85 -0
- package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js +85 -0
- package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js +82 -0
- package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js +1031 -0
- package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js +22 -9
- package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js +47 -0
- package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.js +210 -0
- package/dist/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.js.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js +262 -0
- package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js.map +1 -0
- package/dist/navigation/ledger-root-builder.d.ts +17 -1
- package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
- package/dist/navigation/ledger-root-builder.js +318 -80
- package/dist/navigation/ledger-root-builder.js.map +1 -1
- package/dist/navigation/utils/findNode.d.ts +1 -0
- package/dist/navigation/utils/findNode.d.ts.map +1 -1
- package/dist/navigation/utils/findNode.js +3 -11
- package/dist/navigation/utils/findNode.js.map +1 -1
- package/dist/navigation/utils/followRedirect.d.ts.map +1 -1
- package/dist/navigation/utils/followRedirect.js +2 -0
- package/dist/navigation/utils/followRedirect.js.map +1 -1
- package/dist/orpc-client/docs-ledger/contract.d.ts +50 -0
- package/dist/orpc-client/docs-ledger/contract.d.ts.map +1 -1
- package/dist/orpc-client/docs-ledger/contract.js +1 -0
- package/dist/orpc-client/docs-ledger/contract.js.map +1 -1
- package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
- package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/navigation/__test__/findNode.authedRedirect.test.d.ts +2 -0
- package/dist/types/navigation/__test__/findNode.authedRedirect.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.skipSlugApiReference.test.d.ts.map +1 -0
- package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
- package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
- package/dist/types/navigation/ledger-root-builder.d.ts +17 -1
- package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
- package/dist/types/navigation/utils/findNode.d.ts +1 -0
- package/dist/types/navigation/utils/findNode.d.ts.map +1 -1
- package/dist/types/navigation/utils/followRedirect.d.ts.map +1 -1
- package/dist/types/orpc-client/docs-ledger/contract.d.ts +50 -0
- package/dist/types/orpc-client/docs-ledger/contract.d.ts.map +1 -1
- package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
- package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import * as FernNavigation from "./index.js";
|
|
2
|
+
const STRUCTURAL_SEGMENT_TYPES = new Set([
|
|
3
|
+
"section",
|
|
4
|
+
"apiReference",
|
|
5
|
+
"apiPackage",
|
|
6
|
+
"changelog"
|
|
7
|
+
]);
|
|
8
|
+
const API_NODE_SEGMENT_TYPES = new Set(["apiReference", "apiPackage"]);
|
|
9
|
+
const INFRA_SEGMENT_TYPES = new Set(["files", "redirects"]);
|
|
2
10
|
/** @internal Exported for testing only. */
|
|
3
11
|
export function createBuildContext(basePath) {
|
|
4
12
|
// Tracks how many times each id key has been handed out, so collisions get
|
|
@@ -109,10 +117,7 @@ function toViewers(viewers) {
|
|
|
109
117
|
* cross-variant prev/next links to disclose restricted content.
|
|
110
118
|
*/
|
|
111
119
|
function mergeVariantViewers(seg) {
|
|
112
|
-
const segViewers = seg.metadata.type
|
|
113
|
-
seg.metadata.type === "apiReference" ||
|
|
114
|
-
seg.metadata.type === "apiPackage" ||
|
|
115
|
-
seg.metadata.type === "changelog"
|
|
120
|
+
const segViewers = STRUCTURAL_SEGMENT_TYPES.has(seg.metadata.type)
|
|
116
121
|
? seg.metadata.viewers
|
|
117
122
|
: undefined;
|
|
118
123
|
const variantViewers = seg.variant?.viewers;
|
|
@@ -330,6 +335,63 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
|
|
|
330
335
|
}
|
|
331
336
|
return undefined;
|
|
332
337
|
}
|
|
338
|
+
function apiLeafPositionKey(node) {
|
|
339
|
+
switch (node.type) {
|
|
340
|
+
case "endpoint":
|
|
341
|
+
return `endpoint:${node.endpointId}`;
|
|
342
|
+
case "webhook":
|
|
343
|
+
return `webhook:${node.webhookId}`;
|
|
344
|
+
case "webSocket":
|
|
345
|
+
return `webSocket:${node.webSocketId}`;
|
|
346
|
+
case "grpc":
|
|
347
|
+
return `grpc:${node.grpcId}`;
|
|
348
|
+
case "graphql":
|
|
349
|
+
return `graphql:${node.graphqlOperationId}`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function apiChildPositionKey(node) {
|
|
353
|
+
switch (node.type) {
|
|
354
|
+
case "endpoint":
|
|
355
|
+
case "webhook":
|
|
356
|
+
case "webSocket":
|
|
357
|
+
case "grpc":
|
|
358
|
+
case "graphql":
|
|
359
|
+
return apiLeafPositionKey(node);
|
|
360
|
+
default:
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function positionedApiLeafChildren(ctx, nav, apiDefinitionId) {
|
|
365
|
+
const rawLeafNodes = [];
|
|
366
|
+
const leafPositions = new Map();
|
|
367
|
+
const nonApiLeafNodes = [];
|
|
368
|
+
for (let i = 0; i < nav.length; i++) {
|
|
369
|
+
const route = nav[i];
|
|
370
|
+
const pos = route.displaySortOrder ?? i;
|
|
371
|
+
const apiLeaf = apiLeafNodeFromNavRouteInternal(ctx, route, apiDefinitionId);
|
|
372
|
+
if (apiLeaf != null) {
|
|
373
|
+
rawLeafNodes.push(apiLeaf);
|
|
374
|
+
const key = apiLeafPositionKey(apiLeaf);
|
|
375
|
+
leafPositions.set(key, Math.min(leafPositions.get(key) ?? pos, pos));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const node = artifactToNode(ctx, route);
|
|
379
|
+
if (node?.type === "page" || node?.type === "link") {
|
|
380
|
+
nonApiLeafNodes.push({ node, pos });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const apiLeafNodes = mergeEndpointPairs(ctx, rawLeafNodes, nav);
|
|
384
|
+
const positionedApiLeafNodes = apiLeafNodes.map((node, index) => {
|
|
385
|
+
if (node.type === "endpointPair") {
|
|
386
|
+
const streamPos = leafPositions.get(apiLeafPositionKey(node.stream)) ?? index;
|
|
387
|
+
const nonStreamPos = leafPositions.get(apiLeafPositionKey(node.nonStream)) ?? index;
|
|
388
|
+
return { node, pos: Math.min(streamPos, nonStreamPos) };
|
|
389
|
+
}
|
|
390
|
+
const key = apiChildPositionKey(node);
|
|
391
|
+
return { node, pos: key != null ? (leafPositions.get(key) ?? index) : index };
|
|
392
|
+
});
|
|
393
|
+
return [...positionedApiLeafNodes, ...nonApiLeafNodes];
|
|
394
|
+
}
|
|
333
395
|
/**
|
|
334
396
|
* Collect every endpoint/webhook navigation node from a single API segment's
|
|
335
397
|
* nav routes. The root builder now populates apiReference children directly,
|
|
@@ -344,7 +406,11 @@ export function collectApiLeafNodesFromNav(routes, basePath, segmentApiDefinitio
|
|
|
344
406
|
.map((route) => apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId))
|
|
345
407
|
.filter((node) => node != null);
|
|
346
408
|
}
|
|
347
|
-
/**
|
|
409
|
+
/**
|
|
410
|
+
* Read the apiDefinitionId off a segment's metadata, or undefined.
|
|
411
|
+
* The matched types (apiReference, apiPackage, changelog) are the
|
|
412
|
+
* subset of {@link StructuralSegmentType} that carry an apiDefinitionId.
|
|
413
|
+
*/
|
|
348
414
|
export function segmentApiDefinitionId(metadata) {
|
|
349
415
|
switch (metadata.type) {
|
|
350
416
|
case "apiReference":
|
|
@@ -524,14 +590,10 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
|
|
|
524
590
|
return {
|
|
525
591
|
type: "section",
|
|
526
592
|
...defaults,
|
|
527
|
-
|
|
528
|
-
// so getInitiallyOpenByDefaultNodes picks it up and the UI starts open.
|
|
529
|
-
collapsed: metaCollapsed(seg.metadata) === false
|
|
530
|
-
? "open-by-default"
|
|
531
|
-
: (metaCollapsed(seg.metadata) ?? defaults.collapsed),
|
|
593
|
+
collapsed: metaCollapsed(seg.metadata) ?? defaults.collapsed,
|
|
532
594
|
overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
|
|
533
595
|
noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
|
|
534
|
-
collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata)
|
|
596
|
+
collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
535
597
|
collapsedByDefault: metaBool(seg.metadata, "collapsedByDefault") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
|
|
536
598
|
availability: metaStr(seg.metadata, "availability"),
|
|
537
599
|
pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
|
|
@@ -569,11 +631,10 @@ function directChildApiPackages(parentSection, scopeSegs) {
|
|
|
569
631
|
function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix) {
|
|
570
632
|
const { seg, nav } = scoped;
|
|
571
633
|
// Build leaf nodes from this segment's own nav routes (not descendants).
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const leafNodes = mergeEndpointPairs(ctx, rawLeafNodes, nav);
|
|
634
|
+
// API references can contain normal docs pages and links in addition to
|
|
635
|
+
// endpoint leaves, so include markdown/link artifacts here instead of only
|
|
636
|
+
// API leaf types.
|
|
637
|
+
const leafNodes = positionedApiLeafChildren(ctx, nav, apiDefinitionId);
|
|
577
638
|
// Build nested ApiPackageNodes from direct child apiPackage segments.
|
|
578
639
|
const childPackages = directChildApiPackages(seg.section, scopeSegs);
|
|
579
640
|
const packageNodes = childPackages.map((child) => ({
|
|
@@ -583,18 +644,12 @@ function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix)
|
|
|
583
644
|
// Interleave by position when childIndex metadata is available
|
|
584
645
|
const hasChildIndex = packageNodes.some((p) => p.childIndex != null);
|
|
585
646
|
if (!hasChildIndex) {
|
|
586
|
-
return [...leafNodes, ...packageNodes.map((p) => p.node)]
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
for (const route of nav) {
|
|
590
|
-
if (route.displaySortOrder != null) {
|
|
591
|
-
leafPositions.push(route.displaySortOrder);
|
|
592
|
-
}
|
|
647
|
+
return [...leafNodes, ...packageNodes.map((p) => ({ node: p.node, pos: Number.MAX_SAFE_INTEGER }))]
|
|
648
|
+
.sort((a, b) => a.pos - b.pos)
|
|
649
|
+
.map((p) => p.node);
|
|
593
650
|
}
|
|
594
651
|
const positioned = [];
|
|
595
|
-
|
|
596
|
-
positioned.push({ node: leafNodes[i], pos: leafPositions[i] ?? i });
|
|
597
|
-
}
|
|
652
|
+
positioned.push(...leafNodes);
|
|
598
653
|
for (const p of packageNodes) {
|
|
599
654
|
positioned.push({ node: p.node, pos: p.childIndex ?? Number.MAX_SAFE_INTEGER });
|
|
600
655
|
}
|
|
@@ -803,7 +858,7 @@ function changelogNode(ctx, scoped, scopePrefix) {
|
|
|
803
858
|
pageId: FernNavigation.PageId(metaStr(r.metadata, "pageId") ?? ""),
|
|
804
859
|
noindex: metaBool(r.metadata, "noindex"),
|
|
805
860
|
date: dateStr ?? "",
|
|
806
|
-
tags:
|
|
861
|
+
tags: metaStrArr(r.metadata, "tags")
|
|
807
862
|
};
|
|
808
863
|
let months = byYear.get(year);
|
|
809
864
|
if (months == null) {
|
|
@@ -916,7 +971,7 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
916
971
|
const type = metaStr(s.seg.metadata, "type");
|
|
917
972
|
const childIndex = metaNum(s.seg.metadata, "childIndex");
|
|
918
973
|
let node;
|
|
919
|
-
if (type
|
|
974
|
+
if (API_NODE_SEGMENT_TYPES.has(type ?? "")) {
|
|
920
975
|
node = apiReferenceNode(ctx, s, scopeSegs, scopePrefix);
|
|
921
976
|
}
|
|
922
977
|
else if (type === "changelog") {
|
|
@@ -930,6 +985,54 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
|
|
|
930
985
|
});
|
|
931
986
|
}
|
|
932
987
|
// ── build a sidebarRoot for a single scope ─────────────────────────────────
|
|
988
|
+
/**
|
|
989
|
+
* When multiple root-level sections share the same section path (common for
|
|
990
|
+
* versioned docs where all tab-level sections receive the version slug as
|
|
991
|
+
* their path), we need to scope buildSectionTree so each section only adopts
|
|
992
|
+
* the child segments that physically belong to it.
|
|
993
|
+
*
|
|
994
|
+
* Ownership is determined by sortOrder ranges: a section owns segments whose
|
|
995
|
+
* sortOrder falls between its own and the next sibling section's sortOrder.
|
|
996
|
+
* Returns a map only for sections that have duplicates; unique sections won't
|
|
997
|
+
* appear (and should pass unfiltered scopeSegs to buildSectionTree).
|
|
998
|
+
*/
|
|
999
|
+
function computeSiblingSortBoundaries(ordered) {
|
|
1000
|
+
const result = new Map();
|
|
1001
|
+
// Collect root-level sections grouped by their section path.
|
|
1002
|
+
const sectionPaths = new Set(ordered.map((s) => s.seg.section));
|
|
1003
|
+
const rootSections = [];
|
|
1004
|
+
for (const s of ordered) {
|
|
1005
|
+
const type = metaStr(s.seg.metadata, "type");
|
|
1006
|
+
const sec = s.seg.section;
|
|
1007
|
+
if (type !== "section" || sec === "") {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const parent = parentSectionPath(sec);
|
|
1011
|
+
const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
|
|
1012
|
+
if (isRootLevel) {
|
|
1013
|
+
rootSections.push(s);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Group by section path — only paths with >1 section need boundaries.
|
|
1017
|
+
const byPath = new Map();
|
|
1018
|
+
for (const s of rootSections) {
|
|
1019
|
+
const group = byPath.get(s.seg.section) ?? [];
|
|
1020
|
+
group.push(s);
|
|
1021
|
+
byPath.set(s.seg.section, group);
|
|
1022
|
+
}
|
|
1023
|
+
for (const [, siblings] of byPath) {
|
|
1024
|
+
if (siblings.length <= 1) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
// Siblings are already in sortOrder (from `ordered`).
|
|
1028
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
1029
|
+
const lo = siblings[i].seg.sortOrder;
|
|
1030
|
+
const hi = i + 1 < siblings.length ? siblings[i + 1].seg.sortOrder : Number.MAX_SAFE_INTEGER;
|
|
1031
|
+
result.set(siblings[i], { lo, hi });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
933
1036
|
/**
|
|
934
1037
|
* Assemble the children of a scope's sidebar root in cross-segment order.
|
|
935
1038
|
* `tabRoot` segments contribute their page/link artifacts inline at their
|
|
@@ -957,7 +1060,7 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
957
1060
|
//
|
|
958
1061
|
// hasRootSection = true
|
|
959
1062
|
// │
|
|
960
|
-
// ├─ (A) tabRoot / loose artifacts (sec=="",
|
|
1063
|
+
// ├─ (A) tabRoot / loose artifacts (sec=="", !STRUCTURAL_SEGMENT_TYPES)
|
|
961
1064
|
// │ → emit pages/links inline at their displaySortOrder
|
|
962
1065
|
// │
|
|
963
1066
|
// ├─ (B) the root section itself (sec=="", type=="section")
|
|
@@ -969,14 +1072,27 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
969
1072
|
//
|
|
970
1073
|
// Without a root section, depth-0 sections are emitted directly.
|
|
971
1074
|
const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
|
|
1075
|
+
// ── Sibling section ownership ────────────────────────────────────────
|
|
1076
|
+
//
|
|
1077
|
+
// Multiple root-level sections can share the same section path (e.g.
|
|
1078
|
+
// three sections all with path equal to the version slug). When this
|
|
1079
|
+
// happens, buildSectionTree would incorrectly give ALL child subsections
|
|
1080
|
+
// to each sibling. We resolve ownership via sortOrder: a section owns
|
|
1081
|
+
// segments whose sortOrder falls between its own and the next sibling's.
|
|
1082
|
+
const siblingBoundaries = computeSiblingSortBoundaries(ordered);
|
|
972
1083
|
for (const s of ordered) {
|
|
973
1084
|
const type = metaStr(s.seg.metadata, "type");
|
|
974
1085
|
const sec = s.seg.section;
|
|
975
1086
|
if (hasOwningApiReference(s, scopeSegs)) {
|
|
976
1087
|
continue;
|
|
977
1088
|
}
|
|
978
|
-
|
|
979
|
-
|
|
1089
|
+
if (type === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// (A) Loose artifacts: tabRoot or non-structural segments at section path "".
|
|
1093
|
+
// Structural types need their own node builders in (B)/(C) even when
|
|
1094
|
+
// their section path is "" (which happens with skip-slug: true).
|
|
1095
|
+
if (type === "tabRoot" || (sec === "" && !STRUCTURAL_SEGMENT_TYPES.has(type ?? ""))) {
|
|
980
1096
|
for (const r of s.nav) {
|
|
981
1097
|
const node = artifactToNode(ctx, r);
|
|
982
1098
|
if (node != null) {
|
|
@@ -996,21 +1112,41 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
|
|
|
996
1112
|
// adopted by a root section (B).
|
|
997
1113
|
const parent = parentSectionPath(sec);
|
|
998
1114
|
const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
|
|
999
|
-
|
|
1115
|
+
// Skip depth-0 sections already adopted by the root section wrapper (B),
|
|
1116
|
+
// but NOT structural types with sec="" (skip-slug). buildSectionTree
|
|
1117
|
+
// filters out sec="" segments (line 1249), so they wouldn't be adopted
|
|
1118
|
+
// and must be emitted here directly.
|
|
1119
|
+
if (isRootLevel && hasRootSection && sec !== "") {
|
|
1000
1120
|
continue;
|
|
1001
1121
|
}
|
|
1002
1122
|
if (isRootLevel) {
|
|
1003
1123
|
let node;
|
|
1004
|
-
if (type
|
|
1124
|
+
if (API_NODE_SEGMENT_TYPES.has(type ?? "")) {
|
|
1005
1125
|
node = apiReferenceNode(ctx, s, scopeSegs, scopePrefix);
|
|
1006
1126
|
}
|
|
1007
1127
|
else if (type === "changelog") {
|
|
1008
1128
|
node = changelogNode(ctx, s, changelogScopePrefix ?? scopePrefix);
|
|
1009
1129
|
}
|
|
1010
1130
|
else {
|
|
1011
|
-
|
|
1131
|
+
// Scope child segments to this section's sortOrder range when
|
|
1132
|
+
// sibling sections share the same path.
|
|
1133
|
+
const bounds = siblingBoundaries.get(s);
|
|
1134
|
+
const ownedSegs = bounds != null
|
|
1135
|
+
? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
|
|
1136
|
+
: scopeSegs;
|
|
1137
|
+
node = sectionNode(ctx, s, buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix), scopePrefix);
|
|
1012
1138
|
}
|
|
1013
|
-
|
|
1139
|
+
// Shared/merged sections (paths not rooted in scopePrefix) must not
|
|
1140
|
+
// carry childIndex — their source-version positions would sort them
|
|
1141
|
+
// ahead of native sections. Only applies when sibling sections exist
|
|
1142
|
+
// (multiple sections sharing same path), indicating a versioned site
|
|
1143
|
+
// where shared sections were merged from a sibling version.
|
|
1144
|
+
const isSharedSection = siblingBoundaries.size > 0 && scopePrefix !== "" && sec !== "" && !sec.startsWith(scopePrefix);
|
|
1145
|
+
annotated.push({
|
|
1146
|
+
node,
|
|
1147
|
+
pos: isSharedSection ? undefined : metaNum(s.seg.metadata, "childIndex"),
|
|
1148
|
+
isLeaf: false
|
|
1149
|
+
});
|
|
1014
1150
|
}
|
|
1015
1151
|
}
|
|
1016
1152
|
// Interleave by V1 position only when BOTH leaves and segments carry one.
|
|
@@ -1094,7 +1230,7 @@ function buildSidebarRoot(ctx, scopeSegs, scopePrefix, changelogScopePrefix) {
|
|
|
1094
1230
|
* Build a full (un-pruned) `RootNode` from the decomposed ledger model.
|
|
1095
1231
|
*/
|
|
1096
1232
|
export function buildFullRootFromSegments(options) {
|
|
1097
|
-
const { basePath, title, segments, navBySegment } = options;
|
|
1233
|
+
const { basePath, rootSlug: sourceRootSlug, title, segments, navBySegment, skipSharedSectionMerge } = options;
|
|
1098
1234
|
// Per-call state — never shared across concurrent builds.
|
|
1099
1235
|
const ctx = createBuildContext(basePath);
|
|
1100
1236
|
const scoped = segments
|
|
@@ -1102,10 +1238,10 @@ export function buildFullRootFromSegments(options) {
|
|
|
1102
1238
|
// but guard here too.
|
|
1103
1239
|
.filter((seg) => {
|
|
1104
1240
|
const t = metaStr(seg.metadata, "type");
|
|
1105
|
-
return t
|
|
1241
|
+
return !INFRA_SEGMENT_TYPES.has(t ?? "");
|
|
1106
1242
|
})
|
|
1107
1243
|
.map((seg) => ({ seg, nav: navBySegment.get(seg.segmentHash) ?? [] }));
|
|
1108
|
-
const rootSlug = asSlug(basePath);
|
|
1244
|
+
const rootSlug = asSlug(sourceRootSlug ?? basePath);
|
|
1109
1245
|
// Group by scope identity.
|
|
1110
1246
|
const scopesByKey = new Map();
|
|
1111
1247
|
for (const s of scoped) {
|
|
@@ -1119,10 +1255,10 @@ export function buildFullRootFromSegments(options) {
|
|
|
1119
1255
|
const tabs = uniqueDetails(scoped, "tab");
|
|
1120
1256
|
let child;
|
|
1121
1257
|
if (products.length > 0) {
|
|
1122
|
-
child = buildProductGroup(ctx, products, scoped, rootSlug);
|
|
1258
|
+
child = buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMerge);
|
|
1123
1259
|
}
|
|
1124
1260
|
else if (versions.length > 0) {
|
|
1125
|
-
child = buildVersioned(ctx, versions, scoped, rootSlug, undefined);
|
|
1261
|
+
child = buildVersioned(ctx, versions, scoped, rootSlug, undefined, skipSharedSectionMerge);
|
|
1126
1262
|
}
|
|
1127
1263
|
else {
|
|
1128
1264
|
child = buildUnversioned(ctx, scoped, rootSlug, tabs.length > 0);
|
|
@@ -1277,22 +1413,36 @@ function buildSidebarRootWithVariants(ctx, scopeSegs, scopePrefix, changelogScop
|
|
|
1277
1413
|
if (variants.length <= 1) {
|
|
1278
1414
|
return buildSidebarRoot(ctx, scopeSegs, scopePrefix, changelogScopePrefix);
|
|
1279
1415
|
}
|
|
1416
|
+
const sharedSegs = scopeSegs.filter((s) => s.seg.variant == null);
|
|
1280
1417
|
const variantedNode = buildVariantedNode(ctx, variants, scopeSegs, scopePrefix);
|
|
1418
|
+
const sharedChildren = sharedSegs.length > 0 ? buildSidebarRootChildren(ctx, sharedSegs, scopePrefix, changelogScopePrefix) : [];
|
|
1281
1419
|
const first = scopeSegs[0];
|
|
1282
1420
|
return {
|
|
1283
1421
|
type: "sidebarRoot",
|
|
1284
1422
|
id: ctx.nodeId(`sidebar:${first != null ? scopeKey(first.seg) : "root"}`),
|
|
1285
1423
|
collapsed: undefined,
|
|
1286
|
-
children: [variantedNode]
|
|
1424
|
+
children: [...sharedChildren, variantedNode]
|
|
1287
1425
|
};
|
|
1288
1426
|
}
|
|
1289
1427
|
function buildTabbedOrSidebar(ctx, scopeSegs, tabsPresent, scopePrefix) {
|
|
1290
1428
|
if (!tabsPresent) {
|
|
1291
|
-
// When no tabs exist but variants do,
|
|
1292
|
-
//
|
|
1429
|
+
// When no tabs exist but variants do, wrap variant-scoped segments
|
|
1430
|
+
// in a VariantedNode and include non-variant segments alongside it.
|
|
1293
1431
|
const variants = uniqueDetails(scopeSegs, "variant");
|
|
1294
1432
|
if (variants.length > 1) {
|
|
1295
|
-
|
|
1433
|
+
const sharedSegs = scopeSegs.filter((s) => s.seg.variant == null);
|
|
1434
|
+
const variantedNode = buildVariantedNode(ctx, variants, scopeSegs, scopePrefix);
|
|
1435
|
+
if (sharedSegs.length > 0) {
|
|
1436
|
+
const sharedChildren = buildSidebarRootChildren(ctx, sharedSegs, scopePrefix);
|
|
1437
|
+
const first = scopeSegs[0];
|
|
1438
|
+
return {
|
|
1439
|
+
type: "sidebarRoot",
|
|
1440
|
+
id: ctx.nodeId(`sidebar:${first != null ? scopeKey(first.seg) : "root"}`),
|
|
1441
|
+
collapsed: undefined,
|
|
1442
|
+
children: [...sharedChildren, variantedNode]
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
return variantedNode;
|
|
1296
1446
|
}
|
|
1297
1447
|
return buildSidebarRoot(ctx, scopeSegs, scopePrefix);
|
|
1298
1448
|
}
|
|
@@ -1400,16 +1550,101 @@ function buildUnversioned(ctx, scopeSegs, scopePrefix, tabsPresent) {
|
|
|
1400
1550
|
type: "unversioned",
|
|
1401
1551
|
id: ctx.nodeId(`unversioned:${first?.seg.product?.id ?? "root"}`),
|
|
1402
1552
|
collapsed: undefined,
|
|
1403
|
-
landingPage:
|
|
1553
|
+
landingPage: buildLandingPage(ctx, scopeSegs),
|
|
1404
1554
|
// The enclosing scope prefix (product slug, or rootSlug at the top
|
|
1405
1555
|
// level) roots structural sections so they share the scope's slug
|
|
1406
1556
|
// namespace.
|
|
1407
1557
|
child: buildTabbedOrSidebar(ctx, scopeSegs, tabsPresent, scopePrefix)
|
|
1408
1558
|
};
|
|
1409
1559
|
}
|
|
1410
|
-
|
|
1560
|
+
/**
|
|
1561
|
+
* Merge shared (tab-level) sections from sibling versions into the default
|
|
1562
|
+
* version's segment list. A section is considered "shared" when its section
|
|
1563
|
+
* path does NOT start with any version slug — indicating it was authored at
|
|
1564
|
+
* the tab level and the publish transform assigned it to only one version.
|
|
1565
|
+
*/
|
|
1566
|
+
function mergeSharedSections(defaultSegs, allScoped, defaultVersion, versionSlugs) {
|
|
1567
|
+
// Build a map of the default version's tabs by displayName for
|
|
1568
|
+
// cross-version matching. Tab slugs can't be used because the "default"
|
|
1569
|
+
// tab's slug equals the version slug (e.g. "2026-02-09" vs "2025-04-21"
|
|
1570
|
+
// for the same logical "Documentation" tab across versions).
|
|
1571
|
+
const defaultTabsByName = new Map();
|
|
1572
|
+
for (const s of defaultSegs) {
|
|
1573
|
+
if (s.seg.tab != null) {
|
|
1574
|
+
const tabName = s.seg.tab.displayName;
|
|
1575
|
+
if (!defaultTabsByName.has(tabName)) {
|
|
1576
|
+
defaultTabsByName.set(tabName, s.seg.tab);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
// Collect section paths already present in the default version.
|
|
1581
|
+
const existingSections = new Set(defaultSegs.filter((s) => metaStr(s.seg.metadata, "type") === "section").map((s) => s.seg.section));
|
|
1582
|
+
// Merged sections must sort AFTER all native default segments so they
|
|
1583
|
+
// appear in a trailing sidebarGroup (after non-collapsible sections like
|
|
1584
|
+
// "Getting Started", "Dev Tools", "Capabilities") matching v2 ordering.
|
|
1585
|
+
const maxSortOrder = defaultSegs.reduce((max, s) => Math.max(max, s.seg.sortOrder), 0);
|
|
1586
|
+
let nextSortOrder = maxSortOrder + 1;
|
|
1587
|
+
const merged = [...defaultSegs];
|
|
1588
|
+
for (const s of allScoped) {
|
|
1589
|
+
if (s.seg.version?.id === defaultVersion.id) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const type = metaStr(s.seg.metadata, "type");
|
|
1593
|
+
if (type !== "section") {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const sectionPath = s.seg.section;
|
|
1597
|
+
// Skip version-specific sections (path starts with a version slug).
|
|
1598
|
+
if (versionSlugs.some((vs) => sectionPath === vs || sectionPath.startsWith(vs + "/"))) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
// Skip if the default version already has this section.
|
|
1602
|
+
if (existingSections.has(sectionPath)) {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
// Match the source tab to the default version's corresponding tab
|
|
1606
|
+
// by displayName (tab slugs differ across versions for the default tab).
|
|
1607
|
+
const sourceTabName = s.seg.tab?.displayName ?? "";
|
|
1608
|
+
const targetTab = defaultTabsByName.get(sourceTabName);
|
|
1609
|
+
if (targetTab == null) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
// Re-map the segment's tab and version to the default version's.
|
|
1613
|
+
// Strip childIndex from metadata so merged sections don't sort before
|
|
1614
|
+
// native sections in the annotated interleave step, and assign a
|
|
1615
|
+
// sortOrder after all native segments to place them in the trailing
|
|
1616
|
+
// sidebarGroup.
|
|
1617
|
+
const metadata = s.seg.metadata != null ? { ...s.seg.metadata, childIndex: undefined } : s.seg.metadata;
|
|
1618
|
+
merged.push({
|
|
1619
|
+
seg: {
|
|
1620
|
+
...s.seg,
|
|
1621
|
+
tab: targetTab,
|
|
1622
|
+
version: defaultVersion,
|
|
1623
|
+
sortOrder: nextSortOrder++,
|
|
1624
|
+
metadata
|
|
1625
|
+
},
|
|
1626
|
+
nav: s.nav
|
|
1627
|
+
});
|
|
1628
|
+
existingSections.add(sectionPath);
|
|
1629
|
+
}
|
|
1630
|
+
return merged;
|
|
1631
|
+
}
|
|
1632
|
+
function buildVersioned(ctx, versions, scoped, rootSlug, productPrefix, skipSharedSectionMerge) {
|
|
1633
|
+
// Collect version slugs for detecting version-prefixed section paths.
|
|
1634
|
+
const versionSlugs = versions.map((v) => detailSlug(v)).filter((s) => s !== "");
|
|
1411
1635
|
const children = versions.map((version) => {
|
|
1412
|
-
|
|
1636
|
+
let versionSegs = scoped.filter((s) => s.seg.version?.id === version.id);
|
|
1637
|
+
// Merge shared tab-level sections from sibling versions into the
|
|
1638
|
+
// default version. The publish transform may assign tab-level content
|
|
1639
|
+
// (sections without a version-prefixed path) to a single version;
|
|
1640
|
+
// v2's monolith root includes them in every version's sidebar.
|
|
1641
|
+
// Skipped for scoped roots: the scoped root only loads nav for a
|
|
1642
|
+
// single route's scope, so merged sections carry the source version's
|
|
1643
|
+
// fullPaths into the default tree, creating duplicate slugs that
|
|
1644
|
+
// orphan the real version's pages in NodeCollector.
|
|
1645
|
+
if (detailIsDefault(version) && !skipSharedSectionMerge) {
|
|
1646
|
+
versionSegs = mergeSharedSections(versionSegs, scoped, version, versionSlugs);
|
|
1647
|
+
}
|
|
1413
1648
|
const tabsPresent = uniqueDetails(versionSegs, "tab").length > 0;
|
|
1414
1649
|
const prefix = productPrefix != null ? productPrefix : rootSlug;
|
|
1415
1650
|
// The version node always carries its FULL versioned slug
|
|
@@ -1446,7 +1681,7 @@ function buildVersioned(ctx, versions, scoped, rootSlug, productPrefix) {
|
|
|
1446
1681
|
default: detailIsDefault(version),
|
|
1447
1682
|
versionId: FernNavigation.VersionId(version.id),
|
|
1448
1683
|
availability: undefined,
|
|
1449
|
-
landingPage:
|
|
1684
|
+
landingPage: buildLandingPage(ctx, versionSegs),
|
|
1450
1685
|
announcement: undefined,
|
|
1451
1686
|
pointsTo: metaStr(version.metadata, "pointsTo") != null
|
|
1452
1687
|
? pointsToSlug(metaStr(version.metadata, "pointsTo"))
|
|
@@ -1552,38 +1787,41 @@ function crossVersionDedupKeys(node, parents) {
|
|
|
1552
1787
|
keys.push([...parentTitles, node.title].join("###"));
|
|
1553
1788
|
return keys;
|
|
1554
1789
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
* In V1, these are version-level landing pages that sit outside any tab.
|
|
1558
|
-
*/
|
|
1559
|
-
function buildVersionLandingPage(ctx, versionSegs, versionSlug) {
|
|
1560
|
-
const tablessSegs = versionSegs.filter((s) => s.seg.tab == null);
|
|
1561
|
-
if (tablessSegs.length === 0) {
|
|
1790
|
+
function routeToLandingPage(ctx, route) {
|
|
1791
|
+
if (route == null || route.type !== "markdown" || route.fullPath == null) {
|
|
1562
1792
|
return undefined;
|
|
1563
1793
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const pageId = metaStr(md, "pageId") ?? "";
|
|
1569
|
-
const slug = ctx.contentSlug(r.fullPath);
|
|
1570
|
-
return {
|
|
1571
|
-
type: "landingPage",
|
|
1572
|
-
...withMetadataDefaults({
|
|
1573
|
-
id: ctx.nodeId(`landing:${slug}`),
|
|
1574
|
-
title: metaStr(md, "title") ?? "",
|
|
1575
|
-
slug,
|
|
1576
|
-
icon: metaStr(md, "icon"),
|
|
1577
|
-
hidden: r.hidden,
|
|
1578
|
-
viewers: toViewers(metaStrArr(md, "viewers"))
|
|
1579
|
-
}),
|
|
1580
|
-
pageId: FernNavigation.PageId(pageId),
|
|
1581
|
-
noindex: metaBool(md, "noindex")
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1794
|
+
const md = route.metadata;
|
|
1795
|
+
const pageId = metaStr(md, "pageId");
|
|
1796
|
+
if (pageId == null) {
|
|
1797
|
+
return undefined;
|
|
1585
1798
|
}
|
|
1586
|
-
|
|
1799
|
+
const slug = ctx.contentSlug(route.fullPath);
|
|
1800
|
+
return {
|
|
1801
|
+
type: "landingPage",
|
|
1802
|
+
...withMetadataDefaults({
|
|
1803
|
+
id: ctx.nodeId(`landing:${slug}`),
|
|
1804
|
+
title: metaStr(md, "title") ?? "",
|
|
1805
|
+
slug,
|
|
1806
|
+
icon: metaStr(md, "icon"),
|
|
1807
|
+
hidden: route.hidden,
|
|
1808
|
+
authed: metaBool(md, "authed"),
|
|
1809
|
+
viewers: toViewers(metaStrArr(md, "viewers")),
|
|
1810
|
+
orphaned: metaBool(md, "orphaned"),
|
|
1811
|
+
featureFlags: metaFeatureFlags(md)
|
|
1812
|
+
}),
|
|
1813
|
+
pageId: FernNavigation.PageId(pageId),
|
|
1814
|
+
noindex: metaBool(md, "noindex")
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Build a LandingPageNode from the explicit landing-page segment emitted by
|
|
1819
|
+
* the publish transform. This avoids confusing ordinary loose tabRoot pages
|
|
1820
|
+
* with root/product/version landing pages.
|
|
1821
|
+
*/
|
|
1822
|
+
function buildLandingPage(ctx, scopeSegs) {
|
|
1823
|
+
const landingSegment = scopeSegs.find((s) => metaStr(s.seg.metadata, "type") === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true);
|
|
1824
|
+
return routeToLandingPage(ctx, landingSegment?.nav.find((route) => route.type === "markdown"));
|
|
1587
1825
|
}
|
|
1588
1826
|
/**
|
|
1589
1827
|
* After default-version canonical slugs are marked, propagate them across
|
|
@@ -1629,7 +1867,7 @@ function markCrossVersionCanonicalSlugs(versionNodes) {
|
|
|
1629
1867
|
});
|
|
1630
1868
|
}
|
|
1631
1869
|
}
|
|
1632
|
-
function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
1870
|
+
function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMerge) {
|
|
1633
1871
|
const children = products.map((product) => {
|
|
1634
1872
|
const productSegs = scoped.filter((s) => s.seg.product?.id === product.id);
|
|
1635
1873
|
const versions = uniqueDetails(productSegs, "version");
|
|
@@ -1638,7 +1876,7 @@ function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
|
1638
1876
|
// ProductChild is VersionedNode | UnversionedNode — a version-less
|
|
1639
1877
|
// product wraps its sidebar/tabs in an UnversionedNode.
|
|
1640
1878
|
const productChild = versions.length > 0
|
|
1641
|
-
? buildVersioned(ctx, versions, productSegs, rootSlug, productPrefix)
|
|
1879
|
+
? buildVersioned(ctx, versions, productSegs, rootSlug, productPrefix, skipSharedSectionMerge)
|
|
1642
1880
|
: buildUnversioned(ctx, productSegs, asSlug(productPrefix), tabsPresent);
|
|
1643
1881
|
const node = {
|
|
1644
1882
|
type: "product",
|
|
@@ -1673,7 +1911,7 @@ function buildProductGroup(ctx, products, scoped, rootSlug) {
|
|
|
1673
1911
|
type: "productgroup",
|
|
1674
1912
|
id: ctx.nodeId("productgroup"),
|
|
1675
1913
|
collapsed: undefined,
|
|
1676
|
-
landingPage:
|
|
1914
|
+
landingPage: buildLandingPage(ctx, scoped.filter((s) => s.seg.product == null)),
|
|
1677
1915
|
children
|
|
1678
1916
|
};
|
|
1679
1917
|
}
|