@fern-api/fdr-sdk 1.2.42-d3871329d3 → 1.2.42-e01a8637c3

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.
Files changed (58) hide show
  1. package/dist/js/client/FdrClient.js +1 -0
  2. package/dist/js/client/FdrClient.js.map +1 -1
  3. package/dist/js/client/FdrClient.mjs +1 -0
  4. package/dist/js/client/FdrClient.mjs.map +1 -1
  5. package/dist/js/index.js +1 -0
  6. package/dist/js/index.js.map +1 -1
  7. package/dist/js/index.mjs +1 -0
  8. package/dist/js/index.mjs.map +1 -1
  9. package/dist/js/navigation/ledger-root-builder.js +107 -22
  10. package/dist/js/navigation/ledger-root-builder.js.map +1 -1
  11. package/dist/js/navigation/ledger-root-builder.mjs +107 -22
  12. package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
  13. package/dist/js/orpc-client.js +1 -0
  14. package/dist/js/orpc-client.js.map +1 -1
  15. package/dist/js/orpc-client.mjs +1 -0
  16. package/dist/js/orpc-client.mjs.map +1 -1
  17. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
  18. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
  19. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js +267 -0
  20. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js.map +1 -0
  21. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
  22. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
  23. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js +306 -0
  24. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js.map +1 -0
  25. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts +2 -0
  26. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts.map +1 -0
  27. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.js +273 -0
  28. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.js.map +1 -0
  29. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts +2 -0
  30. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts.map +1 -0
  31. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.js +234 -0
  32. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.js.map +1 -0
  33. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  34. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  35. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js +256 -0
  36. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js.map +1 -0
  37. package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
  38. package/dist/navigation/ledger-root-builder.js +192 -33
  39. package/dist/navigation/ledger-root-builder.js.map +1 -1
  40. package/dist/orpc-client/docs-ledger/contract.d.ts +9 -0
  41. package/dist/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  42. package/dist/orpc-client/docs-ledger/contract.js +1 -0
  43. package/dist/orpc-client/docs-ledger/contract.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
  46. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
  47. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
  48. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
  49. package/dist/types/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts +2 -0
  50. package/dist/types/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts.map +1 -0
  51. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts +2 -0
  52. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts.map +1 -0
  53. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  54. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  55. package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
  56. package/dist/types/orpc-client/docs-ledger/contract.d.ts +9 -0
  57. package/dist/types/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  58. package/package.json +3 -3
@@ -569,6 +569,30 @@ function stripLeadingScopePrefix(section, scopePrefix) {
569
569
  }
570
570
  return section;
571
571
  }
572
+ function computeDuplicateSiblingSortBoundaries(siblings) {
573
+ const result = new Map();
574
+ const byPath = new Map();
575
+ for (const sibling of siblings) {
576
+ if (metaStr(sibling.seg.metadata, "type") !== "section") {
577
+ continue;
578
+ }
579
+ const group = byPath.get(sibling.seg.section) ?? [];
580
+ group.push(sibling);
581
+ byPath.set(sibling.seg.section, group);
582
+ }
583
+ for (const [, group] of byPath) {
584
+ if (group.length <= 1) {
585
+ continue;
586
+ }
587
+ for (let i = 0; i < group.length; i++) {
588
+ result.set(group[i], {
589
+ lo: group[i].seg.sortOrder,
590
+ hi: i + 1 < group.length ? group[i + 1].seg.sortOrder : Number.MAX_SAFE_INTEGER
591
+ });
592
+ }
593
+ }
594
+ return result;
595
+ }
572
596
  /**
573
597
  * Merge leaf children and section children by their original V1 tree position.
574
598
  * Leaves carry position via `displaySortOrder` (stored in nav routes), sections
@@ -642,13 +666,16 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
642
666
  * `parentSectionPath(seg.section) === parentSection`, or is an orphaned segment
643
667
  * whose nearest existing ancestor is `parentSection`.
644
668
  */
645
- function directChildApiPackages(parentSection, scopeSegs) {
669
+ function directChildApiPackages(parentSection, scopeSegs, apiDefinitionId) {
646
670
  const sectionPaths = new Set(scopeSegs.map((s) => s.seg.section));
647
671
  return scopeSegs
648
672
  .filter((s) => {
649
673
  if (metaStr(s.seg.metadata, "type") !== "apiPackage") {
650
674
  return false;
651
675
  }
676
+ if (metaStr(s.seg.metadata, "apiDefinitionId") !== apiDefinitionId) {
677
+ return false;
678
+ }
652
679
  const parent = parentSectionPath(s.seg.section);
653
680
  if (parent === parentSection) {
654
681
  return true;
@@ -673,11 +700,37 @@ function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix)
673
700
  // API leaf types.
674
701
  const leafNodes = positionedApiLeafChildren(ctx, nav, apiDefinitionId);
675
702
  // Build nested ApiPackageNodes from direct child apiPackage segments.
676
- const childPackages = directChildApiPackages(seg.section, scopeSegs);
703
+ let childPackages = directChildApiPackages(seg.section, scopeSegs, apiDefinitionId);
704
+ // When multiple API-type segments share the same section path (skipUrlSlug
705
+ // group packages that collapse to their parent's section), children from
706
+ // different groups get mixed together. Scope children to this segment's DFS
707
+ // subtree using sortOrder boundaries to restore correct ownership.
708
+ const samePathApiSiblings = scopeSegs.filter((s) => s.seg.section === seg.section &&
709
+ s.seg.sortOrder !== seg.sortOrder &&
710
+ API_NODE_SEGMENT_TYPES.has(metaStr(s.seg.metadata, "type") ?? ""));
711
+ const hasCollapsedGroups = samePathApiSiblings.length > 0;
712
+ if (hasCollapsedGroups && metaStr(seg.metadata, "type") !== "apiReference") {
713
+ // This is a group package sharing its section with siblings.
714
+ // Only claim children whose sortOrder falls within our DFS subtree.
715
+ const nextSibling = samePathApiSiblings
716
+ .filter((s) => s.seg.sortOrder > seg.sortOrder)
717
+ .sort((a, b) => a.seg.sortOrder - b.seg.sortOrder)[0];
718
+ const lo = seg.sortOrder;
719
+ const hi = nextSibling?.seg.sortOrder ?? Number.MAX_SAFE_INTEGER;
720
+ childPackages = childPackages.filter((c) => c.seg.sortOrder > lo && c.seg.sortOrder < hi);
721
+ }
677
722
  const packageNodes = childPackages.map((child) => ({
678
723
  node: apiPackageChildNode(ctx, child, scopeSegs, apiDefinitionId, scopePrefix),
679
724
  childIndex: metaNum(child.seg.metadata, "childIndex")
680
725
  }));
726
+ // When collapsed groups are detected at apiReference level, children's
727
+ // childIndex values are relative to different parent groups and cannot be
728
+ // used for interleaving. Fall back to sortOrder (DFS creation order).
729
+ if (hasCollapsedGroups && metaStr(seg.metadata, "type") === "apiReference") {
730
+ return [...leafNodes, ...packageNodes.map((p, i) => ({ node: p.node, pos: leafNodes.length + i }))]
731
+ .sort((a, b) => a.pos - b.pos)
732
+ .map((p) => p.node);
733
+ }
681
734
  // Interleave by position when childIndex metadata is available
682
735
  const hasChildIndex = packageNodes.some((p) => p.childIndex != null);
683
736
  if (!hasChildIndex) {
@@ -834,7 +887,7 @@ function apiReferenceNode(ctx, scoped, scopeSegs, scopePrefix) {
834
887
  availability: metaAvailability(seg.metadata),
835
888
  overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
836
889
  noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
837
- paginated: true,
890
+ paginated: metaBool(seg.metadata, "paginated"),
838
891
  showErrors: metaBool(seg.metadata, "showErrors"),
839
892
  hideTitle: metaBool(seg.metadata, "hideTitle"),
840
893
  pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
@@ -852,7 +905,12 @@ function attachedApiChangelogNode(ctx, scoped, scopeSegs, apiDefinitionId, scope
852
905
  }
853
906
  return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
854
907
  });
855
- const changelogSeg = candidates.find((candidate) => parentSectionPath(candidate.seg.section) === seg.section) ?? candidates[0];
908
+ const apiReferenceCount = scopeSegs.filter((candidate) => {
909
+ return (metaStr(candidate.seg.metadata, "type") === "apiReference" &&
910
+ metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId);
911
+ }).length;
912
+ const changelogSeg = candidates.find((candidate) => parentSectionPath(candidate.seg.section) === seg.section) ??
913
+ (apiReferenceCount === 1 ? candidates[0] : undefined);
856
914
  return changelogSeg != null ? changelogNode(ctx, changelogSeg, scopePrefix) : undefined;
857
915
  }
858
916
  function hasOwningApiReference(scoped, scopeSegs) {
@@ -860,12 +918,14 @@ function hasOwningApiReference(scoped, scopeSegs) {
860
918
  if (apiDefinitionId == null || metaStr(scoped.seg.metadata, "type") !== "changelog") {
861
919
  return false;
862
920
  }
863
- return scopeSegs.some((candidate) => {
921
+ const candidates = scopeSegs.filter((candidate) => {
864
922
  if (metaStr(candidate.seg.metadata, "type") !== "apiReference") {
865
923
  return false;
866
924
  }
867
925
  return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
868
926
  });
927
+ return (candidates.some((candidate) => parentSectionPath(scoped.seg.section) === candidate.seg.section) ||
928
+ candidates.length === 1);
869
929
  }
870
930
  /**
871
931
  * True when this segment is an `apiPackage` that will be nested under an in-scope
@@ -1046,6 +1106,12 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1046
1106
  if (hasOwningApiReference(s, scopeSegs) || isNestedApiPackage(s, scopeSegs)) {
1047
1107
  return false;
1048
1108
  }
1109
+ // Segments marked as sidebar depth 0 are V1 root-level peers and
1110
+ // must never be adopted as children of another section.
1111
+ const depth = metaNum(s.seg.metadata, "sidebarDepth");
1112
+ if (depth === 0) {
1113
+ return false;
1114
+ }
1049
1115
  const parent = parentSectionPath(sec);
1050
1116
  if (parent === parentPath) {
1051
1117
  return true;
@@ -1058,6 +1124,7 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1058
1124
  return false;
1059
1125
  })
1060
1126
  .sort((a, b) => a.seg.sortOrder - b.seg.sortOrder);
1127
+ const siblingBoundaries = computeDuplicateSiblingSortBoundaries(direct);
1061
1128
  return direct.map((s) => {
1062
1129
  const type = metaStr(s.seg.metadata, "type");
1063
1130
  const childIndex = metaNum(s.seg.metadata, "childIndex");
@@ -1070,7 +1137,11 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1070
1137
  }
1071
1138
  else {
1072
1139
  // section (or unrecognised) → recurse for nested sections.
1073
- node = sectionNode(ctx, s, buildSectionTree(ctx, scopeSegs, s.seg.section, scopePrefix, changelogScopePrefix), scopePrefix);
1140
+ const boundary = siblingBoundaries.get(s);
1141
+ const childScopeSegs = boundary != null
1142
+ ? scopeSegs.filter((candidate) => candidate.seg.sortOrder >= boundary.lo && candidate.seg.sortOrder < boundary.hi)
1143
+ : scopeSegs;
1144
+ node = sectionNode(ctx, s, buildSectionTree(ctx, childScopeSegs, s.seg.section, scopePrefix, changelogScopePrefix), scopePrefix);
1074
1145
  }
1075
1146
  return { node, childIndex };
1076
1147
  });
@@ -1105,8 +1176,11 @@ function computeSiblingSortBoundaries(ordered) {
1105
1176
  rootSections.push(s);
1106
1177
  continue;
1107
1178
  }
1179
+ const depth = metaNum(s.seg.metadata, "sidebarDepth");
1108
1180
  const parent = parentSectionPath(sec);
1109
- const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1181
+ const isRootLevel = depth === 0 ||
1182
+ parent === "" ||
1183
+ (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1110
1184
  if (isRootLevel) {
1111
1185
  rootSections.push(s);
1112
1186
  }
@@ -1151,6 +1225,17 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1151
1225
  // segments. Both index into the SAME child list at write time, so they
1152
1226
  // interleave directly (e.g. [page 0, page 1, section 2…15, page 16, 17]).
1153
1227
  const annotated = [];
1228
+ // ── Sibling section ownership ────────────────────────────────────────
1229
+ //
1230
+ // Multiple root-level sections can share the same section path (e.g.
1231
+ // three sections all with path equal to the version slug). When this
1232
+ // happens, buildSectionTree would incorrectly give ALL child subsections
1233
+ // to each sibling. We resolve ownership via sortOrder: a section owns
1234
+ // segments whose sortOrder falls between its own and the next sibling's.
1235
+ //
1236
+ // Computed first because hasRootSection (below) uses it to decide
1237
+ // whether bounded skip-slug siblings should activate adoption.
1238
+ const siblingBoundaries = computeSiblingSortBoundaries(ordered);
1154
1239
  // ── Root-section adoption flow ──────────────────────────────────────
1155
1240
  //
1156
1241
  // A scope may contain a "root section" — a segment with type "section"
@@ -1162,28 +1247,42 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1162
1247
  // │ → emit pages/links inline at their displaySortOrder
1163
1248
  // │
1164
1249
  // ├─ (B) the root section itself (sec=="", type=="section")
1165
- // │ → buildSectionTree adopts ALL depth-0 children as its own
1166
- // │ → emit one sectionNode wrapping the whole subtree
1250
+ // │ → buildSectionTree adopts nested children (depth > 0)
1251
+ // │ → emit one sectionNode wrapping the subtree
1167
1252
  // │
1168
- // └─ (C) depth-0 sections (parentSectionPath=="")
1169
- // → SKIP already adopted by (B) via buildSectionTree
1253
+ // └─ (C) depth-0 sections (sidebarDepth==0 or parentSectionPath=="")
1254
+ // → emit directly as root children (not adopted by B)
1170
1255
  //
1171
1256
  // Without a root section, depth-0 sections are emitted directly.
1172
- const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
1173
- // ── Sibling section ownership ────────────────────────────────────────
1174
1257
  //
1175
- // Multiple root-level sections can share the same section path (e.g.
1176
- // three sections all with path equal to the version slug). When this
1177
- // happens, buildSectionTree would incorrectly give ALL child subsections
1178
- // to each sibling. We resolve ownership via sortOrder: a section owns
1179
- // segments whose sortOrder falls between its own and the next sibling's.
1180
- const siblingBoundaries = computeSiblingSortBoundaries(ordered);
1258
+ // A section with sec=="" AND `childIndex` in its metadata is a sibling
1259
+ // inside a V1 sidebarGroup that has skipUrlSlug not a true structural
1260
+ // wrapper. `walkNavSubTrees` always passes `childIndex`; direct
1261
+ // sidebarRoot children (true wrappers) never receive one.
1262
+ //
1263
+ // Such siblings only activate adoption when they have sortOrder
1264
+ // boundaries (multiple sec="" sections sharing the same path), which
1265
+ // correctly scope each section's children. A *single* unbounded
1266
+ // skip-slug sibling (e.g. mangopay's "Introduction") must NOT adopt
1267
+ // its depth-0 peers — doing so produces spurious breadcrumb entries.
1268
+ const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" &&
1269
+ s.seg.section === "" &&
1270
+ (metaNum(s.seg.metadata, "childIndex") == null || siblingBoundaries.has(s)));
1181
1271
  for (const s of ordered) {
1182
1272
  const type = metaStr(s.seg.metadata, "type");
1183
1273
  const sec = s.seg.section;
1184
1274
  if (hasOwningApiReference(s, scopeSegs) || isNestedApiPackage(s, scopeSegs)) {
1185
1275
  continue;
1186
1276
  }
1277
+ // Skip apiPackage segments that share their section path with an
1278
+ // apiReference sibling (skipUrlSlug structural wrappers). Their
1279
+ // children are already claimed by the apiReference via DFS ordering.
1280
+ if (type === "apiPackage") {
1281
+ const hasApiRefSibling = ordered.some((sib) => sib.seg.section === sec && metaStr(sib.seg.metadata, "type") === "apiReference");
1282
+ if (hasApiRefSibling) {
1283
+ continue;
1284
+ }
1285
+ }
1187
1286
  if (type === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true) {
1188
1287
  continue;
1189
1288
  }
@@ -1200,9 +1299,11 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1200
1299
  continue;
1201
1300
  }
1202
1301
  // (B) Root section wrapper — adopts depth-0 children via buildSectionTree.
1302
+ // Only applies to true root wrappers (no childIndex); sibling
1303
+ // sections with skipUrlSlug (have childIndex) fall through to (C).
1203
1304
  // When multiple sections share sec==="" (skip-slug), scope children
1204
1305
  // to each section's sortOrder range so subsections aren't duplicated.
1205
- if (type === "section" && sec === "") {
1306
+ if (type === "section" && sec === "" && hasRootSection) {
1206
1307
  const bounds = siblingBoundaries.get(s);
1207
1308
  const ownedSegs = bounds != null
1208
1309
  ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
@@ -1214,13 +1315,17 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1214
1315
  }
1215
1316
  // (C) Depth-0 or orphaned sections — emit at root unless already
1216
1317
  // adopted by a root section (B).
1318
+ const depth = metaNum(s.seg.metadata, "sidebarDepth");
1217
1319
  const parent = parentSectionPath(sec);
1218
- const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1219
- // Skip depth-0 sections already adopted by the root section wrapper (B),
1220
- // but NOT structural types with sec="" (skip-slug). buildSectionTree
1221
- // filters out sec="" segments (line 1249), so they wouldn't be adopted
1222
- // and must be emitted here directly.
1223
- if (isRootLevel && hasRootSection && sec !== "") {
1320
+ const isRootLevel = depth === 0 ||
1321
+ parent === "" ||
1322
+ (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1323
+ // Skip depth-0 sections already adopted by the root section wrapper (B).
1324
+ // When sidebarDepth is available, depth-0 sections are V1 root peers
1325
+ // that buildSectionTree now excludes, so they must NOT be skipped.
1326
+ // Only skip sections without sidebarDepth (old data) that would still
1327
+ // be adopted by path-based hierarchy in buildSectionTree.
1328
+ if (isRootLevel && hasRootSection && sec !== "" && depth == null) {
1224
1329
  continue;
1225
1330
  }
1226
1331
  if (isRootLevel) {
@@ -1238,7 +1343,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1238
1343
  const ownedSegs = bounds != null
1239
1344
  ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
1240
1345
  : scopeSegs;
1241
- node = sectionNode(ctx, s, buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix), scopePrefix);
1346
+ // A section with sec="" that reaches here (not adopted as a
1347
+ // root wrapper in case B) is a skipUrlSlug sibling.
1348
+ // When sortOrder boundaries exist (multiple sec="" sections),
1349
+ // the scoped ownedSegs correctly limits which subsections
1350
+ // each section can adopt. When there are NO boundaries
1351
+ // (single sec="" section, e.g. mangopay's "Introduction"),
1352
+ // buildSectionTree("") would re-adopt all depth-0 peers;
1353
+ // pass an empty child list instead — its leaf artifacts are
1354
+ // already in its own segment nav.
1355
+ const isUnboundedSkipSlugSibling = sec === "" && metaNum(s.seg.metadata, "childIndex") != null && bounds == null;
1356
+ const sectionChildren = isUnboundedSkipSlugSibling
1357
+ ? []
1358
+ : buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix);
1359
+ node = sectionNode(ctx, s, sectionChildren, scopePrefix);
1242
1360
  }
1243
1361
  // Shared/merged sections (paths not rooted in scopePrefix) must not
1244
1362
  // carry childIndex — their source-version positions would sort them
@@ -1308,11 +1426,6 @@ function groupSidebarRootChildren(ctx, children) {
1308
1426
  flushRun();
1309
1427
  out.push(child);
1310
1428
  }
1311
- else if (child.type === "changelog") {
1312
- flushRun();
1313
- run.push(child);
1314
- flushRun();
1315
- }
1316
1429
  else {
1317
1430
  run.push(child);
1318
1431
  }
@@ -1479,7 +1592,7 @@ function buildVariantedNode(ctx, variants, scopeSegs, scopePrefix) {
1479
1592
  ? asSlug(FernNavigation.slugjoin(scopePrefix, variantIndividualSlug))
1480
1593
  : scopePrefix;
1481
1594
  const metaPointsTo = metaStr(variant.metadata, "pointsTo");
1482
- const children = buildSidebarRootChildren(ctx, variantSegs, scopePrefix);
1595
+ const children = buildSidebarRootChildren(ctx, variantSegs, variantSlug);
1483
1596
  return {
1484
1597
  type: "variant",
1485
1598
  ...withMetadataDefaults({
@@ -2015,6 +2128,14 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
2015
2128
  };
2016
2129
  return node;
2017
2130
  });
2131
+ // Cross-product canonical slug deduplication (V2 parity).
2132
+ // V2's migrator deduplicates pages across products via a global pageId map:
2133
+ // the first product to register a pageId wins, and subsequent products'
2134
+ // pages with the same pageId receive a canonicalSlug pointing to the
2135
+ // canonical (default-product) page. Without this, shared content that is
2136
+ // materialized under every product scope (e.g. merge-api-basics pages)
2137
+ // appears as separate sitemap entries per product.
2138
+ markCrossProductCanonicalSlugs(children);
2018
2139
  return {
2019
2140
  type: "productgroup",
2020
2141
  id: ctx.nodeId("productgroup"),
@@ -2023,5 +2144,43 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
2023
2144
  children
2024
2145
  };
2025
2146
  }
2147
+ /**
2148
+ * Deduplicate pages across products using pageId as a content-identity key.
2149
+ * Pages with the same pageId in different products are the same content
2150
+ * materialized per-scope; the default product's slug becomes canonical and
2151
+ * non-default copies receive `canonicalSlug` so the sitemap collapses them.
2152
+ *
2153
+ * API leaf nodes (endpoints, websockets, etc.) are unaffected — `getPageId`
2154
+ * returns undefined for those types, so product-specific API surface is
2155
+ * never suppressed.
2156
+ */
2157
+ function markCrossProductCanonicalSlugs(productNodes) {
2158
+ const pageIdToCanonical = new Map();
2159
+ // Process default product first so its slugs become the canonical targets.
2160
+ const sorted = [...productNodes].sort((a, b) => {
2161
+ if (a.default !== b.default) {
2162
+ return a.default ? -1 : 1;
2163
+ }
2164
+ return 0;
2165
+ });
2166
+ for (const product of sorted) {
2167
+ FernNavigation.traverseDF(product, (child) => {
2168
+ if (FernNavigation.hasMetadata(child) && FernNavigation.isPage(child)) {
2169
+ const pid = FernNavigation.getPageId(child);
2170
+ if (pid == null) {
2171
+ return;
2172
+ }
2173
+ const key = `pid:${pid}`;
2174
+ const canonical = pageIdToCanonical.get(key);
2175
+ if (canonical != null && !product.default) {
2176
+ child.canonicalSlug = canonical;
2177
+ }
2178
+ else if (canonical == null) {
2179
+ pageIdToCanonical.set(key, child.canonicalSlug ?? child.slug);
2180
+ }
2181
+ }
2182
+ });
2183
+ }
2184
+ }
2026
2185
  // (root keeps the bare "root" id; it is the single tree root.)
2027
2186
  //# sourceMappingURL=ledger-root-builder.js.map