@fern-api/fdr-sdk 1.2.42-0c024f5b0d → 1.2.42-23647d2912

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 (32) hide show
  1. package/dist/js/navigation/ledger-root-builder.js +134 -30
  2. package/dist/js/navigation/ledger-root-builder.js.map +1 -1
  3. package/dist/js/navigation/ledger-root-builder.mjs +134 -30
  4. package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
  5. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
  6. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
  7. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js +267 -0
  8. package/dist/navigation/__test__/ledger-root-builder.crossProductDedup.test.js.map +1 -0
  9. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
  10. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
  11. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js +306 -0
  12. package/dist/navigation/__test__/ledger-root-builder.ledgerParity.test.js.map +1 -0
  13. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js +109 -4
  14. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js.map +1 -1
  15. package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js +102 -0
  16. package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
  17. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  18. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  19. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js +256 -0
  20. package/dist/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.js.map +1 -0
  21. package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
  22. package/dist/navigation/ledger-root-builder.js +192 -33
  23. package/dist/navigation/ledger-root-builder.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts +2 -0
  26. package/dist/types/navigation/__test__/ledger-root-builder.crossProductDedup.test.d.ts.map +1 -0
  27. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts +2 -0
  28. package/dist/types/navigation/__test__/ledger-root-builder.ledgerParity.test.d.ts.map +1 -0
  29. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts +2 -0
  30. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugSiblingSection.test.d.ts.map +1 -0
  31. package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
  32. package/package.json +1 -1
@@ -60,6 +60,41 @@ function metaStr(metadata, key) {
60
60
  }
61
61
  return undefined;
62
62
  }
63
+ /**
64
+ * V1 availability values stored in ledger metadata are lowercase (e.g. "deprecated"),
65
+ * but the latest Availability type uses PascalCase (e.g. "Deprecated"). This map
66
+ * normalises both formats so the badge component can look up display names correctly.
67
+ */
68
+ const AVAILABILITY_LOOKUP = {
69
+ // V1 lowercase values (stored by docsPublishTransform from V1 navigation tree)
70
+ alpha: FernNavigation.Availability.Alpha,
71
+ stable: FernNavigation.Availability.Stable,
72
+ "generally-available": FernNavigation.Availability.GenerallyAvailable,
73
+ "in-development": FernNavigation.Availability.InDevelopment,
74
+ "pre-release": FernNavigation.Availability.PreRelease,
75
+ deprecated: FernNavigation.Availability.Deprecated,
76
+ beta: FernNavigation.Availability.Beta,
77
+ preview: FernNavigation.Availability.Preview,
78
+ legacy: FernNavigation.Availability.Legacy,
79
+ // Latest PascalCase values (pass-through)
80
+ Alpha: FernNavigation.Availability.Alpha,
81
+ Stable: FernNavigation.Availability.Stable,
82
+ GenerallyAvailable: FernNavigation.Availability.GenerallyAvailable,
83
+ InDevelopment: FernNavigation.Availability.InDevelopment,
84
+ PreRelease: FernNavigation.Availability.PreRelease,
85
+ Deprecated: FernNavigation.Availability.Deprecated,
86
+ Beta: FernNavigation.Availability.Beta,
87
+ Preview: FernNavigation.Availability.Preview,
88
+ Legacy: FernNavigation.Availability.Legacy
89
+ };
90
+ /** Read an availability field from metadata, converting V1 lowercase to latest PascalCase. */
91
+ function metaAvailability(metadata) {
92
+ const raw = metaStr(metadata, "availability");
93
+ if (raw == null) {
94
+ return undefined;
95
+ }
96
+ return AVAILABILITY_LOOKUP[raw];
97
+ }
63
98
  function metaBool(metadata, key) {
64
99
  if (metadata != null && typeof metadata === "object" && key in metadata) {
65
100
  const v = metadata[key];
@@ -198,7 +233,7 @@ function artifactToNode(ctx, route) {
198
233
  }),
199
234
  pageId: FernNavigation.PageId(pageId),
200
235
  noindex: metaBool(md, "noindex"),
201
- availability: metaStr(md, "availability")
236
+ availability: metaAvailability(md)
202
237
  };
203
238
  return node;
204
239
  }
@@ -269,7 +304,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
269
304
  endpointId: FernNavigation.EndpointId(endpointId),
270
305
  apiDefinitionId,
271
306
  isResponseStream: metaBool(md, "isResponseStream"),
272
- availability: metaStr(md, "availability"),
307
+ availability: metaAvailability(md),
273
308
  playground: undefined
274
309
  };
275
310
  }
@@ -285,7 +320,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
285
320
  method: method,
286
321
  webhookId: FernNavigation.WebhookId(webhookId),
287
322
  apiDefinitionId,
288
- availability: metaStr(md, "availability")
323
+ availability: metaAvailability(md)
289
324
  };
290
325
  }
291
326
  if (route.type === "asyncapi") {
@@ -298,7 +333,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
298
333
  ...meta,
299
334
  webSocketId: FernNavigation.WebSocketId(webSocketId),
300
335
  apiDefinitionId,
301
- availability: metaStr(md, "availability"),
336
+ availability: metaAvailability(md),
302
337
  playground: undefined
303
338
  };
304
339
  }
@@ -314,7 +349,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
314
349
  method: method,
315
350
  grpcId: FernNavigation.GrpcId(grpcId),
316
351
  apiDefinitionId,
317
- availability: metaStr(md, "availability")
352
+ availability: metaAvailability(md)
318
353
  };
319
354
  }
320
355
  if (route.type === "graphql") {
@@ -330,7 +365,7 @@ function apiLeafNodeFromNavRouteInternal(ctx, route, segmentApiDefinitionId) {
330
365
  graphqlOperationId: FernNavigation.GraphQlOperationId(graphqlOperationId),
331
366
  graphqlOperationIds: undefined,
332
367
  apiDefinitionId,
333
- availability: metaStr(md, "availability"),
368
+ availability: metaAvailability(md),
334
369
  playground: undefined
335
370
  };
336
371
  }
@@ -534,6 +569,30 @@ function stripLeadingScopePrefix(section, scopePrefix) {
534
569
  }
535
570
  return section;
536
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
+ }
537
596
  /**
538
597
  * Merge leaf children and section children by their original V1 tree position.
539
598
  * Leaves carry position via `displaySortOrder` (stored in nav routes), sections
@@ -597,7 +656,7 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
597
656
  noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
598
657
  collapsible: metaBool(seg.metadata, "collapsible") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
599
658
  collapsedByDefault: metaBool(seg.metadata, "collapsedByDefault") ?? (metaCollapsed(seg.metadata) === true ? true : undefined),
600
- availability: metaStr(seg.metadata, "availability"),
659
+ availability: metaAvailability(seg.metadata),
601
660
  pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
602
661
  children: mergeChildrenByPosition(ctx, nav, sectionChildren)
603
662
  };
@@ -607,13 +666,16 @@ function sectionNode(ctx, scoped, sectionChildren, scopePrefix) {
607
666
  * `parentSectionPath(seg.section) === parentSection`, or is an orphaned segment
608
667
  * whose nearest existing ancestor is `parentSection`.
609
668
  */
610
- function directChildApiPackages(parentSection, scopeSegs) {
669
+ function directChildApiPackages(parentSection, scopeSegs, apiDefinitionId) {
611
670
  const sectionPaths = new Set(scopeSegs.map((s) => s.seg.section));
612
671
  return scopeSegs
613
672
  .filter((s) => {
614
673
  if (metaStr(s.seg.metadata, "type") !== "apiPackage") {
615
674
  return false;
616
675
  }
676
+ if (metaStr(s.seg.metadata, "apiDefinitionId") !== apiDefinitionId) {
677
+ return false;
678
+ }
617
679
  const parent = parentSectionPath(s.seg.section);
618
680
  if (parent === parentSection) {
619
681
  return true;
@@ -638,7 +700,7 @@ function buildApiChildren(ctx, scoped, scopeSegs, apiDefinitionId, scopePrefix)
638
700
  // API leaf types.
639
701
  const leafNodes = positionedApiLeafChildren(ctx, nav, apiDefinitionId);
640
702
  // Build nested ApiPackageNodes from direct child apiPackage segments.
641
- const childPackages = directChildApiPackages(seg.section, scopeSegs);
703
+ const childPackages = directChildApiPackages(seg.section, scopeSegs, apiDefinitionId);
642
704
  const packageNodes = childPackages.map((child) => ({
643
705
  node: apiPackageChildNode(ctx, child, scopeSegs, apiDefinitionId, scopePrefix),
644
706
  childIndex: metaNum(child.seg.metadata, "childIndex")
@@ -686,7 +748,7 @@ function apiPackageChildNode(ctx, scoped, scopeSegs, parentApiDefinitionId, scop
686
748
  featureFlags: metaFeatureFlags(seg.metadata)
687
749
  }),
688
750
  apiDefinitionId: FernNavigation.ApiDefinitionId(apiDefinitionId),
689
- availability: metaStr(seg.metadata, "availability"),
751
+ availability: metaAvailability(seg.metadata),
690
752
  overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
691
753
  noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
692
754
  pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
@@ -796,10 +858,10 @@ function apiReferenceNode(ctx, scoped, scopeSegs, scopePrefix) {
796
858
  featureFlags: metaFeatureFlags(seg.metadata)
797
859
  }),
798
860
  apiDefinitionId: FernNavigation.ApiDefinitionId(apiDefinitionId),
799
- availability: metaStr(seg.metadata, "availability"),
861
+ availability: metaAvailability(seg.metadata),
800
862
  overviewPageId: overviewPageId != null ? FernNavigation.PageId(overviewPageId) : undefined,
801
863
  noindex: metaBool(overview?.metadata, "noindex") ?? metaBool(seg.metadata, "noindex"),
802
- paginated: true,
864
+ paginated: metaBool(seg.metadata, "paginated"),
803
865
  showErrors: metaBool(seg.metadata, "showErrors"),
804
866
  hideTitle: metaBool(seg.metadata, "hideTitle"),
805
867
  pointsTo: metaPointsTo != null ? pointsToSlug(metaPointsTo) : undefined,
@@ -817,7 +879,12 @@ function attachedApiChangelogNode(ctx, scoped, scopeSegs, apiDefinitionId, scope
817
879
  }
818
880
  return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
819
881
  });
820
- const changelogSeg = candidates.find((candidate) => parentSectionPath(candidate.seg.section) === seg.section) ?? candidates[0];
882
+ const apiReferenceCount = scopeSegs.filter((candidate) => {
883
+ return (metaStr(candidate.seg.metadata, "type") === "apiReference" &&
884
+ metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId);
885
+ }).length;
886
+ const changelogSeg = candidates.find((candidate) => parentSectionPath(candidate.seg.section) === seg.section) ??
887
+ (apiReferenceCount === 1 ? candidates[0] : undefined);
821
888
  return changelogSeg != null ? changelogNode(ctx, changelogSeg, scopePrefix) : undefined;
822
889
  }
823
890
  function hasOwningApiReference(scoped, scopeSegs) {
@@ -825,12 +892,14 @@ function hasOwningApiReference(scoped, scopeSegs) {
825
892
  if (apiDefinitionId == null || metaStr(scoped.seg.metadata, "type") !== "changelog") {
826
893
  return false;
827
894
  }
828
- return scopeSegs.some((candidate) => {
895
+ const candidates = scopeSegs.filter((candidate) => {
829
896
  if (metaStr(candidate.seg.metadata, "type") !== "apiReference") {
830
897
  return false;
831
898
  }
832
899
  return metaStr(candidate.seg.metadata, "apiDefinitionId") === apiDefinitionId;
833
900
  });
901
+ return (candidates.some((candidate) => parentSectionPath(scoped.seg.section) === candidate.seg.section) ||
902
+ candidates.length === 1);
834
903
  }
835
904
  /**
836
905
  * True when this segment is an `apiPackage` that will be nested under an in-scope
@@ -1023,6 +1092,7 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1023
1092
  return false;
1024
1093
  })
1025
1094
  .sort((a, b) => a.seg.sortOrder - b.seg.sortOrder);
1095
+ const siblingBoundaries = computeDuplicateSiblingSortBoundaries(direct);
1026
1096
  return direct.map((s) => {
1027
1097
  const type = metaStr(s.seg.metadata, "type");
1028
1098
  const childIndex = metaNum(s.seg.metadata, "childIndex");
@@ -1035,7 +1105,11 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1035
1105
  }
1036
1106
  else {
1037
1107
  // section (or unrecognised) → recurse for nested sections.
1038
- node = sectionNode(ctx, s, buildSectionTree(ctx, scopeSegs, s.seg.section, scopePrefix, changelogScopePrefix), scopePrefix);
1108
+ const boundary = siblingBoundaries.get(s);
1109
+ const childScopeSegs = boundary != null
1110
+ ? scopeSegs.filter((candidate) => candidate.seg.sortOrder >= boundary.lo && candidate.seg.sortOrder < boundary.hi)
1111
+ : scopeSegs;
1112
+ node = sectionNode(ctx, s, buildSectionTree(ctx, childScopeSegs, s.seg.section, scopePrefix, changelogScopePrefix), scopePrefix);
1039
1113
  }
1040
1114
  return { node, childIndex };
1041
1115
  });
@@ -1055,12 +1129,19 @@ function buildSectionTree(ctx, scopeSegs, parentPath, scopePrefix, changelogScop
1055
1129
  function computeSiblingSortBoundaries(ordered) {
1056
1130
  const result = new Map();
1057
1131
  // Collect root-level sections grouped by their section path.
1132
+ // Sections with sec==="" (skip-slug sections) are included: when multiple
1133
+ // skip-slug sections exist at the root, they share path "" and need
1134
+ // boundaries so each only adopts its own children.
1058
1135
  const sectionPaths = new Set(ordered.map((s) => s.seg.section));
1059
1136
  const rootSections = [];
1060
1137
  for (const s of ordered) {
1061
1138
  const type = metaStr(s.seg.metadata, "type");
1062
1139
  const sec = s.seg.section;
1063
- if (type !== "section" || sec === "") {
1140
+ if (type !== "section") {
1141
+ continue;
1142
+ }
1143
+ if (sec === "") {
1144
+ rootSections.push(s);
1064
1145
  continue;
1065
1146
  }
1066
1147
  const parent = parentSectionPath(sec);
@@ -1109,6 +1190,17 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1109
1190
  // segments. Both index into the SAME child list at write time, so they
1110
1191
  // interleave directly (e.g. [page 0, page 1, section 2…15, page 16, 17]).
1111
1192
  const annotated = [];
1193
+ // ── Sibling section ownership ────────────────────────────────────────
1194
+ //
1195
+ // Multiple root-level sections can share the same section path (e.g.
1196
+ // three sections all with path equal to the version slug). When this
1197
+ // happens, buildSectionTree would incorrectly give ALL child subsections
1198
+ // to each sibling. We resolve ownership via sortOrder: a section owns
1199
+ // segments whose sortOrder falls between its own and the next sibling's.
1200
+ //
1201
+ // Computed first because hasRootSection (below) uses it to decide
1202
+ // whether bounded skip-slug siblings should activate adoption.
1203
+ const siblingBoundaries = computeSiblingSortBoundaries(ordered);
1112
1204
  // ── Root-section adoption flow ──────────────────────────────────────
1113
1205
  //
1114
1206
  // A scope may contain a "root section" — a segment with type "section"
@@ -1127,15 +1219,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1127
1219
  // → SKIP — already adopted by (B) via buildSectionTree
1128
1220
  //
1129
1221
  // Without a root section, depth-0 sections are emitted directly.
1130
- const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" && s.seg.section === "");
1131
- // ── Sibling section ownership ────────────────────────────────────────
1132
1222
  //
1133
- // Multiple root-level sections can share the same section path (e.g.
1134
- // three sections all with path equal to the version slug). When this
1135
- // happens, buildSectionTree would incorrectly give ALL child subsections
1136
- // to each sibling. We resolve ownership via sortOrder: a section owns
1137
- // segments whose sortOrder falls between its own and the next sibling's.
1138
- const siblingBoundaries = computeSiblingSortBoundaries(ordered);
1223
+ // A section with sec=="" AND `childIndex` in its metadata is a sibling
1224
+ // inside a V1 sidebarGroup that has skipUrlSlug not a true structural
1225
+ // wrapper. `walkNavSubTrees` always passes `childIndex`; direct
1226
+ // sidebarRoot children (true wrappers) never receive one.
1227
+ //
1228
+ // Such siblings only activate adoption when they have sortOrder
1229
+ // boundaries (multiple sec="" sections sharing the same path), which
1230
+ // correctly scope each section's children. A *single* unbounded
1231
+ // skip-slug sibling (e.g. mangopay's "Introduction") must NOT adopt
1232
+ // its depth-0 peers — doing so produces spurious breadcrumb entries.
1233
+ const hasRootSection = ordered.some((s) => metaStr(s.seg.metadata, "type") === "section" &&
1234
+ s.seg.section === "" &&
1235
+ (metaNum(s.seg.metadata, "childIndex") == null || siblingBoundaries.has(s)));
1139
1236
  for (const s of ordered) {
1140
1237
  const type = metaStr(s.seg.metadata, "type");
1141
1238
  const sec = s.seg.section;
@@ -1158,8 +1255,16 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1158
1255
  continue;
1159
1256
  }
1160
1257
  // (B) Root section wrapper — adopts depth-0 children via buildSectionTree.
1161
- if (type === "section" && sec === "") {
1162
- const sectionChildren = buildSectionTree(ctx, scopeSegs, "", scopePrefix);
1258
+ // Only applies to true root wrappers (no childIndex); sibling
1259
+ // sections with skipUrlSlug (have childIndex) fall through to (C).
1260
+ // When multiple sections share sec==="" (skip-slug), scope children
1261
+ // to each section's sortOrder range so subsections aren't duplicated.
1262
+ if (type === "section" && sec === "" && hasRootSection) {
1263
+ const bounds = siblingBoundaries.get(s);
1264
+ const ownedSegs = bounds != null
1265
+ ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
1266
+ : scopeSegs;
1267
+ const sectionChildren = buildSectionTree(ctx, ownedSegs, "", scopePrefix, changelogScopePrefix);
1163
1268
  const node = sectionNode(ctx, s, sectionChildren, scopePrefix);
1164
1269
  annotated.push({ node, pos: metaNum(s.seg.metadata, "childIndex"), isLeaf: false });
1165
1270
  continue;
@@ -1190,7 +1295,20 @@ function buildSidebarRootChildren(ctx, scopeSegs, scopePrefix, changelogScopePre
1190
1295
  const ownedSegs = bounds != null
1191
1296
  ? scopeSegs.filter((seg) => seg.seg.sortOrder >= bounds.lo && seg.seg.sortOrder < bounds.hi)
1192
1297
  : scopeSegs;
1193
- node = sectionNode(ctx, s, buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix), scopePrefix);
1298
+ // A section with sec="" that reaches here (not adopted as a
1299
+ // root wrapper in case B) is a skipUrlSlug sibling.
1300
+ // When sortOrder boundaries exist (multiple sec="" sections),
1301
+ // the scoped ownedSegs correctly limits which subsections
1302
+ // each section can adopt. When there are NO boundaries
1303
+ // (single sec="" section, e.g. mangopay's "Introduction"),
1304
+ // buildSectionTree("") would re-adopt all depth-0 peers;
1305
+ // pass an empty child list instead — its leaf artifacts are
1306
+ // already in its own segment nav.
1307
+ const isUnboundedSkipSlugSibling = sec === "" && metaNum(s.seg.metadata, "childIndex") != null && bounds == null;
1308
+ const sectionChildren = isUnboundedSkipSlugSibling
1309
+ ? []
1310
+ : buildSectionTree(ctx, ownedSegs, sec, scopePrefix, changelogScopePrefix);
1311
+ node = sectionNode(ctx, s, sectionChildren, scopePrefix);
1194
1312
  }
1195
1313
  // Shared/merged sections (paths not rooted in scopePrefix) must not
1196
1314
  // carry childIndex — their source-version positions would sort them
@@ -1260,11 +1378,6 @@ function groupSidebarRootChildren(ctx, children) {
1260
1378
  flushRun();
1261
1379
  out.push(child);
1262
1380
  }
1263
- else if (child.type === "changelog") {
1264
- flushRun();
1265
- run.push(child);
1266
- flushRun();
1267
- }
1268
1381
  else {
1269
1382
  run.push(child);
1270
1383
  }
@@ -1431,7 +1544,7 @@ function buildVariantedNode(ctx, variants, scopeSegs, scopePrefix) {
1431
1544
  ? asSlug(FernNavigation.slugjoin(scopePrefix, variantIndividualSlug))
1432
1545
  : scopePrefix;
1433
1546
  const metaPointsTo = metaStr(variant.metadata, "pointsTo");
1434
- const children = buildSidebarRootChildren(ctx, variantSegs, scopePrefix);
1547
+ const children = buildSidebarRootChildren(ctx, variantSegs, variantSlug);
1435
1548
  return {
1436
1549
  type: "variant",
1437
1550
  ...withMetadataDefaults({
@@ -1967,6 +2080,14 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
1967
2080
  };
1968
2081
  return node;
1969
2082
  });
2083
+ // Cross-product canonical slug deduplication (V2 parity).
2084
+ // V2's migrator deduplicates pages across products via a global pageId map:
2085
+ // the first product to register a pageId wins, and subsequent products'
2086
+ // pages with the same pageId receive a canonicalSlug pointing to the
2087
+ // canonical (default-product) page. Without this, shared content that is
2088
+ // materialized under every product scope (e.g. merge-api-basics pages)
2089
+ // appears as separate sitemap entries per product.
2090
+ markCrossProductCanonicalSlugs(children);
1970
2091
  return {
1971
2092
  type: "productgroup",
1972
2093
  id: ctx.nodeId("productgroup"),
@@ -1975,5 +2096,43 @@ function buildProductGroup(ctx, products, scoped, rootSlug, skipSharedSectionMer
1975
2096
  children
1976
2097
  };
1977
2098
  }
2099
+ /**
2100
+ * Deduplicate pages across products using pageId as a content-identity key.
2101
+ * Pages with the same pageId in different products are the same content
2102
+ * materialized per-scope; the default product's slug becomes canonical and
2103
+ * non-default copies receive `canonicalSlug` so the sitemap collapses them.
2104
+ *
2105
+ * API leaf nodes (endpoints, websockets, etc.) are unaffected — `getPageId`
2106
+ * returns undefined for those types, so product-specific API surface is
2107
+ * never suppressed.
2108
+ */
2109
+ function markCrossProductCanonicalSlugs(productNodes) {
2110
+ const pageIdToCanonical = new Map();
2111
+ // Process default product first so its slugs become the canonical targets.
2112
+ const sorted = [...productNodes].sort((a, b) => {
2113
+ if (a.default !== b.default) {
2114
+ return a.default ? -1 : 1;
2115
+ }
2116
+ return 0;
2117
+ });
2118
+ for (const product of sorted) {
2119
+ FernNavigation.traverseDF(product, (child) => {
2120
+ if (FernNavigation.hasMetadata(child) && FernNavigation.isPage(child)) {
2121
+ const pid = FernNavigation.getPageId(child);
2122
+ if (pid == null) {
2123
+ return;
2124
+ }
2125
+ const key = `pid:${pid}`;
2126
+ const canonical = pageIdToCanonical.get(key);
2127
+ if (canonical != null && !product.default) {
2128
+ child.canonicalSlug = canonical;
2129
+ }
2130
+ else if (canonical == null) {
2131
+ pageIdToCanonical.set(key, child.canonicalSlug ?? child.slug);
2132
+ }
2133
+ }
2134
+ });
2135
+ }
2136
+ }
1978
2137
  // (root keeps the bare "root" id; it is the single tree root.)
1979
2138
  //# sourceMappingURL=ledger-root-builder.js.map