Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/content-indexer/types/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

interface BaseNavItem {
title: string;
hidden?: boolean;
}

interface PageNavItem extends BaseNavItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ describe("visitApiReference", () => {
expect(firstPath).toBe("reference/get-balance");
});

test("should return no nav for hidden API", () => {
test("should mark nav as hidden for hidden API", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();

Expand Down Expand Up @@ -215,7 +215,8 @@ describe("visitApiReference", () => {
navigationAncestors: [],
});

expect(result.navItem).toBeUndefined();
expect(result.navItem).toBeDefined();
expect(result.navItem).toHaveProperty("hidden", true);
expect(Object.keys(result.indexEntries).length).toBeGreaterThan(0); // Index still created
});

Expand Down
9 changes: 7 additions & 2 deletions src/content-indexer/visitors/__tests__/visit-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("visitPage", () => {
});
});

test("should skip nav item for hidden page", () => {
test("should mark nav item as hidden for hidden page", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();

Expand All @@ -74,7 +74,12 @@ describe("visitPage", () => {
navigationAncestors: [],
});

expect(result.navItem).toBeUndefined();
expect(result.navItem).toEqual({
title: "Hidden Page",
path: "/guides/hidden-page",
type: "page",
hidden: true,
});
expect(result.indexEntries["guides/hidden-page"]).toBeDefined(); // Index still created
});

Expand Down
93 changes: 91 additions & 2 deletions src/content-indexer/visitors/__tests__/visit-section.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe("visitSection", () => {
expect(result.indexEntries["guides/page"]).toBeDefined();
});

test("should handle hidden section", () => {
test("should mark hidden section with hidden flag", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();

Expand All @@ -207,11 +207,100 @@ describe("visitSection", () => {
visitNavigationItem,
);

expect(result.navItem).toBeUndefined();
expect(result.navItem).toBeDefined();
expect(result.navItem).toHaveProperty("hidden", true);
expect(result.navItem).toHaveProperty("type", "section");
expect(result.navItem).toHaveProperty("title", "Hidden Section");
// Children should still be present
if (
result.navItem &&
!Array.isArray(result.navItem) &&
"children" in result.navItem
) {
expect(result.navItem.children).toHaveLength(1);
}
// Index entries should still be created
expect(Object.keys(result.indexEntries).length).toBeGreaterThan(0);
});

test("should mark section hidden when all children are hidden", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();

const result = visitSection(
{
item: {
section: "Looks Visible",
contents: [
{
page: "Hidden A",
path: "fern/guides/a.mdx",
hidden: true,
},
{
page: "Hidden B",
path: "fern/guides/b.mdx",
hidden: true,
},
],
},
parentPath: PathBuilder.init("guides"),
tab: "guides",
stripPathPrefix: undefined,
contentCache: cache,
context,
navigationAncestors: [],
},
visitNavigationItem,
);

// Section itself should be marked hidden since all children are hidden
expect(result.navItem).toBeDefined();
expect(result.navItem).toHaveProperty("hidden", true);
// Children should still be present
if (
result.navItem &&
!Array.isArray(result.navItem) &&
"children" in result.navItem
) {
expect(result.navItem.children).toHaveLength(2);
}
});

test("should not mark section hidden when some children are visible", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();

const result = visitSection(
{
item: {
section: "Mixed Section",
contents: [
{
page: "Hidden Page",
path: "fern/guides/hidden.mdx",
hidden: true,
},
{
page: "Visible Page",
path: "fern/guides/visible.mdx",
},
],
},
parentPath: PathBuilder.init("guides"),
tab: "guides",
stripPathPrefix: undefined,
contentCache: cache,
context,
navigationAncestors: [],
},
visitNavigationItem,
);

expect(result.navItem).toBeDefined();
expect(result.navItem).not.toHaveProperty("hidden");
});

test("should recursively process nested sections", () => {
const context = new ProcessingContext("docs");
const cache = new ContentCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe("processOpenRpcSpec", () => {
expect(Array.isArray(result.navItem)).toBe(true);
});

test("should not create nav if hidden", () => {
test("should mark nav as hidden if hidden", () => {
const context = new ProcessingContext("docs");

const result = processOpenRpcSpec({
Expand Down Expand Up @@ -187,7 +187,8 @@ describe("processOpenRpcSpec", () => {
isFlattened: false,
});

expect(result.navItem).toBeUndefined();
expect(result.navItem).toBeDefined();
expect(result.navItem).toHaveProperty("hidden", true);
expect(Object.keys(result.indexEntries).length).toBeGreaterThan(0); // Index still created
});

Expand Down
19 changes: 8 additions & 11 deletions src/content-indexer/visitors/processors/process-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,11 @@ export const processOpenApiSpec = ({
tab,
});

// Return early if hidden (index only, no navigation)
if (isHidden) {
return { indexEntries, navItem: undefined };
}

// Create breadcrumb for Algolia
const apiSectionBreadcrumb = isFlattened
? undefined
: createBreadcrumbNavItem(apiTitle, "api-section");
// Create breadcrumb for Algolia (skip for hidden APIs)
const apiSectionBreadcrumb =
isHidden || isFlattened
? undefined
: createBreadcrumbNavItem(apiTitle, "api-section");

// Build navigation items
const tagSections = buildOpenApiNavigation({
Expand All @@ -205,13 +201,14 @@ export const processOpenApiSpec = ({
isHidden,
});

// Return flattened or wrapped navigation
// Return flattened or wrapped navigation (marked hidden if applicable)
const navItem: NavItem | NavItem[] = isFlattened
? tagSections
? tagSections.map((item) => (isHidden ? { ...item, hidden: true } : item))
: {
title: apiTitle,
type: "api-section",
children: tagSections,
...(isHidden && { hidden: true }),
};

return { indexEntries, navItem };
Expand Down
12 changes: 5 additions & 7 deletions src/content-indexer/visitors/processors/process-openrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,16 @@ export const processOpenRpcSpec = ({
});
});

// Return early if hidden
if (isHidden) {
return { indexEntries, navItem: undefined };
}

// Return flattened or wrapped navigation
// Return flattened or wrapped navigation (marked hidden if applicable)
const navItem: NavItem | NavItem[] = isFlattened
? endpointNavItems
? endpointNavItems.map((item) =>
isHidden ? { ...item, hidden: true } : item,
)
: {
title: apiTitle,
type: "api-section",
children: endpointNavItems,
...(isHidden && { hidden: true }),
};

return { indexEntries, navItem };
Expand Down
19 changes: 9 additions & 10 deletions src/content-indexer/visitors/visit-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,22 @@ export const visitPage = ({
},
};

// Build nav item (skip if hidden)
// Build nav item (marked hidden if applicable)
const descriptionRaw =
cached?.frontmatter.description || cached?.frontmatter.subtitle;
const description =
typeof descriptionRaw === "string" ? descriptionRaw : undefined;

const navItem: NavItem | undefined = pageItem.hidden
? undefined
: {
title: pageItem.page,
path: `/${finalPath}`,
type: "page",
description,
};
const navItem: NavItem = {
title: pageItem.page,
path: `/${finalPath}`,
type: "page",
description,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description is always emitted as a property even when it is undefined, which means navItem will contain description: undefined. This can break strict deep-equality expectations and produces noisier serialized output. Consider only including description when it’s defined (e.g., via conditional object spread), and update the related test expectations accordingly.

Suggested change
description,
...(description !== undefined ? { description } : {}),

Copilot uses AI. Check for mistakes.
...(pageItem.hidden && { hidden: true }),
};

// Build Algolia record (if content available and not hidden)
if (cached && navItem) {
if (cached && !pageItem.hidden) {
const title = cached.frontmatter.title || pageItem.page;
context.addAlgoliaRecord({
pageType: "Guide",
Expand Down
12 changes: 9 additions & 3 deletions src/content-indexer/visitors/visit-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,19 @@ export const visitSection = (
.flat()
.filter((child): child is NavItem => child !== undefined);

// Only include section in nav if it has children and is not hidden
if (children.length === 0 || sectionItem.hidden) {
// Skip section from nav if it has no children
if (children.length === 0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude sections whose children are all hidden

Now that hidden pages/API items are emitted with hidden: true, children can be non-empty even when every child is hidden. This guard only checks children.length === 0, so a non-hidden section with only hidden descendants is still emitted as visible, which can surface empty section headers once consumers filter hidden items. The section-visibility check should consider whether any child is actually visible (or mark the section hidden when all descendants are hidden).

Useful? React with 👍 / 👎.

return { indexEntries, navItem: undefined };
}

// Update section nav item with children
// Update section nav item with children.
// Mark hidden if explicitly hidden OR if every child is hidden
// (prevents empty section headers when consumers filter hidden items).
sectionNavItem.children = children;
const allChildrenHidden = children.every((child) => child.hidden === true);
if (sectionItem.hidden || allChildrenHidden) {
sectionNavItem.hidden = true;
}

return { indexEntries, navItem: sectionNavItem };
};
Loading