fix(docs): breadcrumb APIs only return category/docs items, ignoring links (#11616)
Some checks failed
Argos CI / take-screenshots (push) Waiting to run
Build Hash Router / Build Hash Router (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Continuous Releases / Continuous Releases (push) Waiting to run
E2E Tests / E2E — Yarn v1 (20) (push) Waiting to run
E2E Tests / E2E — Yarn v1 (20.0) (push) Waiting to run
E2E Tests / E2E — Yarn v1 (22) (push) Waiting to run
E2E Tests / E2E — Yarn v1 (24) (push) Waiting to run
E2E Tests / E2E — Yarn v1 (25.1) (push) Waiting to run
E2E Tests / E2E — Yarn v1 Windows (push) Waiting to run
E2E Tests / E2E — Yarn Berry (node-modules, -s) (push) Waiting to run
E2E Tests / E2E — Yarn Berry (node-modules, -st) (push) Waiting to run
E2E Tests / E2E — Yarn Berry (pnp, -s) (push) Waiting to run
E2E Tests / E2E — Yarn Berry (pnp, -st) (push) Waiting to run
E2E Tests / E2E — npm (push) Waiting to run
E2E Tests / E2E — pnpm (push) Waiting to run
Canary Release / Publish Canary (push) Has been cancelled

Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
Cesar Garcia 2025-12-22 13:04:14 -03:00 committed by GitHub
parent 47a98a1d6e
commit 7f5d6122d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 134 additions and 35 deletions

View File

@ -568,13 +568,28 @@ describe('useSidebarBreadcrumbs', () => {
it('returns first level link', () => { it('returns first level link', () => {
const pathname = '/somePathName'; const pathname = '/somePathName';
const sidebar = [testCategory(), testLink({href: pathname})]; const sidebar = [testCategory(), testLink({href: pathname, docId: 'doc1'})];
expect(createUseSidebarBreadcrumbsMock(sidebar)(pathname)).toEqual([ expect(createUseSidebarBreadcrumbsMock(sidebar)(pathname)).toEqual([
sidebar[1], sidebar[1],
]); ]);
}); });
it('returns doc links only', () => {
const pathname = '/somePathName';
// A link that is not a doc link should not appear in the breadcrumbs
// See https://github.com/facebook/docusaurus/pull/11616
const nonDocLink = testLink({href: pathname});
const docLink = testLink({href: pathname, docId: 'doc1'});
const sidebar = [testCategory(), nonDocLink, docLink];
expect(createUseSidebarBreadcrumbsMock(sidebar)(pathname)).toEqual([
docLink,
]);
});
it('returns nested category', () => { it('returns nested category', () => {
const pathname = '/somePathName'; const pathname = '/somePathName';
@ -613,7 +628,7 @@ describe('useSidebarBreadcrumbs', () => {
it('returns nested link', () => { it('returns nested link', () => {
const pathname = '/somePathName'; const pathname = '/somePathName';
const link = testLink({href: pathname}); const link = testLink({href: pathname, docId: 'docNested'});
const categoryLevel3 = testCategory({ const categoryLevel3 = testCategory({
items: [testLink(), link, testLink()], items: [testLink(), link, testLink()],
@ -657,6 +672,35 @@ describe('useSidebarBreadcrumbs', () => {
createUseSidebarBreadcrumbsMock(undefined, false)('/foo'), createUseSidebarBreadcrumbsMock(undefined, false)('/foo'),
).toBeNull(); ).toBeNull();
}); });
// Regression test for https://github.com/facebook/docusaurus/issues/11612
it('returns the category that owns the URL, not a category with a link pointing to it', () => {
const categoryA: PropSidebarItemCategory = testCategory({
label: 'Category A',
href: '/category-a',
items: [
testLink({href: '/category-a/doc1', label: 'Doc 1'}),
testLink({href: '/category-a/doc2', label: 'Doc 2'}),
// This link points to Category B's generated-index
testLink({href: '/category-b', label: 'Go to Category B'}),
],
});
const categoryB: PropSidebarItemCategory = testCategory({
label: 'Category B',
href: '/category-b',
items: [
testLink({href: '/category-b/item1', label: 'Item 1'}),
testLink({href: '/category-b/item2', label: 'Item 2'}),
],
});
const sidebar: PropSidebar = [categoryA, categoryB];
expect(createUseSidebarBreadcrumbsMock(sidebar)('/category-b')).toEqual([
categoryB,
]);
});
}); });
describe('useCurrentSidebarCategory', () => { describe('useCurrentSidebarCategory', () => {
@ -708,12 +752,16 @@ describe('useCurrentSidebarCategory', () => {
expect(mockUseCurrentSidebarCategory('/cat2')).toEqual(category2); expect(mockUseCurrentSidebarCategory('/cat2')).toEqual(category2);
}); });
it('works for category link item', () => { it('works for category doc link item', () => {
const link = testLink({href: '/my/link/path'}); const pathname = '/my/link/path';
const nonDocLink = testLink({href: pathname});
const docLink = testLink({href: pathname, docId: 'doc1'});
const category: PropSidebarItemCategory = testCategory({ const category: PropSidebarItemCategory = testCategory({
href: '/cat1', href: '/cat1',
items: [testLink(), testLink(), link, testCategory()], items: [testLink(), testLink(), nonDocLink, docLink, testCategory()],
}); });
const sidebar: PropSidebar = [ const sidebar: PropSidebar = [
testLink(), testLink(),
testLink(), testLink(),
@ -724,18 +772,28 @@ describe('useCurrentSidebarCategory', () => {
const mockUseCurrentSidebarCategory = const mockUseCurrentSidebarCategory =
createUseCurrentSidebarCategoryMock(sidebar); createUseCurrentSidebarCategoryMock(sidebar);
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual(category); expect(mockUseCurrentSidebarCategory(pathname)).toEqual(category);
}); });
it('works for nested category link item', () => { it('works for nested category link item', () => {
const link = testLink({href: '/my/link/path'}); const pathname = '/my/link/path';
const nonDocLink = testLink({href: pathname});
const docLink = testLink({href: pathname, docId: 'doc1'});
const category2: PropSidebarItemCategory = testCategory({ const category2: PropSidebarItemCategory = testCategory({
href: '/cat2', href: '/cat2',
items: [testLink(), testLink(), link, testCategory()], items: [
testLink(),
testLink(),
testCategory({items: [nonDocLink]}),
nonDocLink,
docLink,
testCategory(),
],
}); });
const category1: PropSidebarItemCategory = testCategory({ const category1: PropSidebarItemCategory = testCategory({
href: '/cat1', href: '/cat1',
items: [testLink(), testLink(), category2, testCategory()], items: [testLink(), nonDocLink, testLink(), category2, testCategory()],
}); });
const sidebar: PropSidebar = [ const sidebar: PropSidebar = [
testLink(), testLink(),
@ -780,6 +838,38 @@ describe('useCurrentSidebarCategory', () => {
`"Unexpected: cant find current sidebar in context"`, `"Unexpected: cant find current sidebar in context"`,
); );
}); });
// Regression test for https://github.com/facebook/docusaurus/issues/11612
it('returns the category that owns the URL, not a category with a link pointing to it', () => {
const categoryA: PropSidebarItemCategory = testCategory({
label: 'Category A',
href: '/category-a',
items: [
testLink({href: '/category-a/doc1', label: 'Doc 1'}),
testLink({href: '/category-a/doc2', label: 'Doc 2'}),
// This link points to Category B's generated-index
testLink({href: '/category-b', label: 'Go to Category B'}),
],
});
const categoryB: PropSidebarItemCategory = testCategory({
label: 'Category B',
href: '/category-b',
items: [
testLink({href: '/category-b/item1', label: 'Item 1'}),
testLink({href: '/category-b/item2', label: 'Item 2'}),
],
});
const sidebar: PropSidebar = [categoryA, categoryB];
const mockUseCurrentSidebarCategory =
createUseCurrentSidebarCategoryMock(sidebar);
// When visiting /category-b, we should get Category B (the owner),
// not Category A (which just has a link to it)
expect(mockUseCurrentSidebarCategory('/category-b')).toEqual(categoryB);
});
}); });
describe('useCurrentSidebarSiblings', () => { describe('useCurrentSidebarSiblings', () => {
@ -805,10 +895,10 @@ describe('useCurrentSidebarSiblings', () => {
testCategory(), testCategory(),
]; ];
const mockUseCurrentSidebarCategory = const mockUseCurrentSidebarSiblings =
createUseCurrentSidebarSiblingsMock(sidebar); createUseCurrentSidebarSiblingsMock(sidebar);
expect(mockUseCurrentSidebarCategory('/cat')).toEqual(category.items); expect(mockUseCurrentSidebarSiblings('/cat')).toEqual(category.items);
}); });
it('works for sidebar root', () => { it('works for sidebar root', () => {
@ -823,10 +913,10 @@ describe('useCurrentSidebarSiblings', () => {
testCategory(), testCategory(),
]; ];
const mockUseCurrentSidebarCategory = const mockUseCurrentSidebarSiblings =
createUseCurrentSidebarSiblingsMock(sidebar); createUseCurrentSidebarSiblingsMock(sidebar);
expect(mockUseCurrentSidebarCategory('/rootLink')).toEqual(sidebar); expect(mockUseCurrentSidebarSiblings('/rootLink')).toEqual(sidebar);
}); });
it('works for nested sidebar category', () => { it('works for nested sidebar category', () => {
@ -852,10 +942,13 @@ describe('useCurrentSidebarSiblings', () => {
}); });
it('works for category link item', () => { it('works for category link item', () => {
const link = testLink({href: '/my/link/path'}); const pathname = '/my/link/path';
const nonDocLink = testLink({href: pathname});
const docLink = testLink({href: pathname, docId: 'doc1'});
const category: PropSidebarItemCategory = testCategory({ const category: PropSidebarItemCategory = testCategory({
href: '/cat1', href: '/cat1',
items: [testLink(), testLink(), link, testCategory()], items: [testLink(), testLink(), nonDocLink, docLink, testCategory()],
}); });
const sidebar: PropSidebar = [ const sidebar: PropSidebar = [
testLink(), testLink(),
@ -864,23 +957,24 @@ describe('useCurrentSidebarSiblings', () => {
testCategory(), testCategory(),
]; ];
const mockUseCurrentSidebarCategory = const mockUseCurrentSidebarSiblings =
createUseCurrentSidebarSiblingsMock(sidebar); createUseCurrentSidebarSiblingsMock(sidebar);
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual( expect(mockUseCurrentSidebarSiblings(pathname)).toEqual(category.items);
category.items,
);
}); });
it('works for nested category link item', () => { it('works for nested category link item', () => {
const link = testLink({href: '/my/link/path'}); const pathname = '/my/link/path';
const nonDocLink = testLink({href: pathname});
const docLink = testLink({href: pathname, docId: 'doc1'});
const category2: PropSidebarItemCategory = testCategory({ const category2: PropSidebarItemCategory = testCategory({
href: '/cat2', href: '/cat2',
items: [testLink(), testLink(), link, testCategory()], items: [testLink(), testLink(), nonDocLink, testCategory()],
}); });
const category1: PropSidebarItemCategory = testCategory({ const category1: PropSidebarItemCategory = testCategory({
href: '/cat1', href: '/cat1',
items: [testLink(), testLink(), category2, testCategory()], items: [testLink(), testLink(), category2, docLink, testCategory()],
}); });
const sidebar: PropSidebar = [ const sidebar: PropSidebar = [
testLink(), testLink(),
@ -889,18 +983,16 @@ describe('useCurrentSidebarSiblings', () => {
testCategory(), testCategory(),
]; ];
const mockUseCurrentSidebarCategory = const mockUseCurrentSidebarSiblings =
createUseCurrentSidebarSiblingsMock(sidebar); createUseCurrentSidebarSiblingsMock(sidebar);
expect(mockUseCurrentSidebarCategory('/my/link/path')).toEqual( expect(mockUseCurrentSidebarSiblings(pathname)).toEqual(category1.items);
category2.items,
);
}); });
it('throws when sidebar is missing', () => { it('throws when sidebar is missing', () => {
const mockUseCurrentSidebarCategory = createUseCurrentSidebarSiblingsMock(); const mockUseCurrentSidebarSiblings = createUseCurrentSidebarSiblingsMock();
expect(() => expect(() =>
mockUseCurrentSidebarCategory('/cat'), mockUseCurrentSidebarSiblings('/cat'),
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unexpected: cant find current sidebar in context"`, `"Unexpected: cant find current sidebar in context"`,
); );

View File

@ -234,15 +234,22 @@ function getSidebarBreadcrumbs({
}): PropSidebarBreadcrumbsItem[] { }): PropSidebarBreadcrumbsItem[] {
const breadcrumbs: PropSidebarBreadcrumbsItem[] = []; const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
function extract(items: PropSidebarItem[]) { function extract(items: PropSidebarItem[]): boolean {
for (const item of items) { for (const item of items) {
if ( // Extract category item
(item.type === 'category' && if (item.type === 'category') {
(isSamePath(item.href, pathname) || extract(item.items))) || if (isSamePath(item.href, pathname) || extract(item.items)) {
(item.type === 'link' && isSamePath(item.href, pathname)) breadcrumbs.unshift(item);
return true;
}
}
// Extract doc item
else if (
item.type === 'link' &&
item.docId &&
isSamePath(item.href, pathname)
) { ) {
const filtered = onlyCategories && item.type !== 'category'; if (!onlyCategories) {
if (!filtered) {
breadcrumbs.unshift(item); breadcrumbs.unshift(item);
} }
return true; return true;