@fern-api/fdr-sdk 1.2.42-b174eece86 → 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 (60) 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 +115 -24
  10. package/dist/js/navigation/ledger-root-builder.js.map +1 -1
  11. package/dist/js/navigation/ledger-root-builder.mjs +115 -24
  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.rootChildOrder.test.js +102 -0
  26. package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
  27. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts +2 -0
  28. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts.map +1 -0
  29. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.js +273 -0
  30. package/dist/navigation/__test__/ledger-root-builder.sidebarDepth.test.js.map +1 -0
  31. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts +2 -0
  32. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts.map +1 -0
  33. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.js +234 -0
  34. package/dist/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.js.map +1 -0
  35. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  36. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  37. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js +256 -0
  38. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js.map +1 -0
  39. package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
  40. package/dist/navigation/ledger-root-builder.js +207 -35
  41. package/dist/navigation/ledger-root-builder.js.map +1 -1
  42. package/dist/orpc-client/docs-ledger/contract.d.ts +9 -0
  43. package/dist/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  44. package/dist/orpc-client/docs-ledger/contract.js +1 -0
  45. package/dist/orpc-client/docs-ledger/contract.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
  48. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
  49. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
  50. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
  51. package/dist/types/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts +2 -0
  52. package/dist/types/navigation/__test__/ledger-root-builder.sidebarDepth.test.d.ts.map +1 -0
  53. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts +2 -0
  54. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugGroupOrdering.test.d.ts.map +1 -0
  55. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  56. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  57. package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
  58. package/dist/types/orpc-client/docs-ledger/contract.d.ts +9 -0
  59. package/dist/types/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  60. 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
  });
@@ -1090,16 +1161,26 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1090
1161
  function computeSiblingSortBoundaries(ordered) {
1091
1162
  const result = new Map();
1092
1163
  // Collect root-level sections grouped by their section path.
1164
+ // Sections with sec==="" (skip-slug sections) are included: when multiple
1165
+ // skip-slug sections exist at the root, they share path "" and need
1166
+ // boundaries so each only adopts its own children.
1093
1167
  const sectionPaths = new Set(ordered.map((s) => s.seg.section));
1094
1168
  const rootSections = [];
1095
1169
  for (const s of ordered) {
1096
1170
  const type = metaStr(s.seg.metadata, "type");
1097
1171
  const sec = s.seg.section;
1098
- if (type !== "section" || sec === "") {
1172
+ if (type !== "section") {
1173
+ continue;
1174
+ }
1175
+ if (sec === "") {
1176
+ rootSections.push(s);
1099
1177
  continue;
1100
1178
  }
1179
+ const depth = metaNum(s.seg.metadata, "sidebarDepth");
1101
1180
  const parent = parentSectionPath(sec);
1102
- const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1181
+ const isRootLevel = depth === 0 ||
1182
+ parent === "" ||
1183
+ (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1103
1184
  if (isRootLevel) {
1104
1185
  rootSections.push(s);
1105
1186
  }
@@ -1144,6 +1225,17 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1144
1225
  // segments. Both index into the SAME child list at write time, so they
1145
1226
  // interleave directly (e.g. [page 0, page 1, section 2…15, page 16, 17]).
1146
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);
1147
1239
  // ── Root-section adoption flow ──────────────────────────────────────
1148
1240
  //
1149
1241
  // A scope may contain a "root section" — a segment with type "section"
@@ -1155,28 +1247,42 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1155
1247
  // │ → emit pages/links inline at their displaySortOrder
1156
1248
  // │
1157
1249
  // ├─ (B) the root section itself (sec=="", type=="section")
1158
- // │ → buildSectionTree adopts ALL depth-0 children as its own
1159
- // │ → emit one sectionNode wrapping the whole subtree
1250
+ // │ → buildSectionTree adopts nested children (depth > 0)
1251
+ // │ → emit one sectionNode wrapping the subtree
1160
1252
  // │
1161
- // └─ (C) depth-0 sections (parentSectionPath=="")
1162
- // → 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)
1163
1255
  //
1164
1256
  // Without a root section, depth-0 sections are emitted directly.
1165
- const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
1166
- // ── Sibling section ownership ────────────────────────────────────────
1167
1257
  //
1168
- // Multiple root-level sections can share the same section path (e.g.
1169
- // three sections all with path equal to the version slug). When this
1170
- // happens, buildSectionTree would incorrectly give ALL child subsections
1171
- // to each sibling. We resolve ownership via sortOrder: a section owns
1172
- // segments whose sortOrder falls between its own and the next sibling's.
1173
- 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)));
1174
1271
  for (const s of ordered) {
1175
1272
  const type = metaStr(s.seg.metadata, "type");
1176
1273
  const sec = s.seg.section;
1177
1274
  if (hasOwningApiReference(s, scopeSegs) || isNestedApiPackage(s, scopeSegs)) {
1178
1275
  continue;
1179
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
+ }
1180
1286
  if (type === "tabRoot" && metaBool(s.seg.metadata, "landingPage") === true) {
1181
1287
  continue;
1182
1288
  }
@@ -1193,21 +1299,33 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1193
1299
  continue;
1194
1300
  }
1195
1301
  // (B) Root section wrapper — adopts depth-0 children via buildSectionTree.
1196
- if (type === "section" && sec === "") {
1197
- const sectionChildren = buildSectionTree(ctx, scopeSegs, "", scopePrefix);
1302
+ // Only applies to true root wrappers (no childIndex); sibling
1303
+ // sections with skipUrlSlug (have childIndex) fall through to (C).
1304
+ // When multiple sections share sec==="" (skip-slug), scope children
1305
+ // to each section's sortOrder range so subsections aren't duplicated.
1306
+ if (type === "section" && sec === "" && hasRootSection) {
1307
+ const bounds = siblingBoundaries.get(s);
1308
+ const ownedSegs = bounds != null
1309
+ ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
1310
+ : scopeSegs;
1311
+ const sectionChildren = buildSectionTree(ctx, ownedSegs, "", scopePrefix, changelogScopePrefix);
1198
1312
  const node = sectionNode(ctx, s, sectionChildren, scopePrefix);
1199
1313
  annotated.push({ node, pos: metaNum(s.seg.metadata, "childIndex"), isLeaf: false });
1200
1314
  continue;
1201
1315
  }
1202
1316
  // (C) Depth-0 or orphaned sections — emit at root unless already
1203
1317
  // adopted by a root section (B).
1318
+ const depth = metaNum(s.seg.metadata, "sidebarDepth");
1204
1319
  const parent = parentSectionPath(sec);
1205
- const isRootLevel = parent === "" || (!sectionPaths.has(parent) && nearestExistingAncestor(sec, sectionPaths) === "");
1206
- // Skip depth-0 sections already adopted by the root section wrapper (B),
1207
- // but NOT structural types with sec="" (skip-slug). buildSectionTree
1208
- // filters out sec="" segments (line 1249), so they wouldn't be adopted
1209
- // and must be emitted here directly.
1210
- 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) {
1211
1329
  continue;
1212
1330
  }
1213
1331
  if (isRootLevel) {
@@ -1225,7 +1343,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1225
1343
  const ownedSegs = bounds != null
1226
1344
  ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
1227
1345
  : scopeSegs;
1228
- 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);
1229
1360
  }
1230
1361
  // Shared/merged sections (paths not rooted in scopePrefix) must not
1231
1362
  // carry childIndex — their source-version positions would sort them
@@ -1295,11 +1426,6 @@ function groupSidebarRootChildren(ctx, children) {
1295
1426
  flushRun();
1296
1427
  out.push(child);
1297
1428
  }
1298
- else if (child.type === "changelog") {
1299
- flushRun();
1300
- run.push(child);
1301
- flushRun();
1302
- }
1303
1429
  else {
1304
1430
  run.push(child);
1305
1431
  }
@@ -1466,7 +1592,7 @@ function buildVariantedNode(ctx, variants, scopeSegs, scopePrefix) {
1466
1592
  ? asSlug(FernNavigation.slugjoin(scopePrefix, variantIndividualSlug))
1467
1593
  : scopePrefix;
1468
1594
  const metaPointsTo = metaStr(variant.metadata, "pointsTo");
1469
- const children = buildSidebarRootChildren(ctx, variantSegs, scopePrefix);
1595
+ const children = buildSidebarRootChildren(ctx, variantSegs, variantSlug);
1470
1596
  return {
1471
1597
  type: "variant",
1472
1598
  ...withMetadataDefaults({
@@ -2002,6 +2128,14 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
2002
2128
  };
2003
2129
  return node;
2004
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);
2005
2139
  return {
2006
2140
  type: "productgroup",
2007
2141
  id: ctx.nodeId("productgroup"),
@@ -2010,5 +2144,43 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
2010
2144
  children
2011
2145
  };
2012
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
+ }
2013
2185
  // (root keeps the bare "root" id; it is the single tree root.)
2014
2186
  //# sourceMappingURL=ledger-root-builder.js.map