Core: Improve Breadcrumbs API

This commit is contained in:
Fuma Nama
2025-09-24 15:14:40 +08:00
parent 168760caa3
commit 51268ec35a
8 changed files with 71 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Breadcrumbs API: Fix root folders being filtered when `includeRoot` is set to `true`.
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': minor
---
Breadcrumbs API: default `includePage` to `false`.
+2 -2
View File
@@ -22,8 +22,8 @@ export async function GET(): Promise<Response> {
url: page.url,
title: page.data.title,
description: page.data.description,
breadcrumbs: items.flatMap<string>((item) =>
typeof item.name === 'string' ? item.name : [],
breadcrumbs: items.flatMap<string>((item, i) =>
i > 0 && typeof item.name === 'string' ? item.name : [],
),
} satisfies OramaDocument;
});
+8 -6
View File
@@ -20,7 +20,6 @@ import type { z } from 'zod';
import { DefaultChatTransport } from 'ai';
import { Markdown } from './markdown';
import { Presence } from '@radix-ui/react-presence';
import { useEffectEvent } from 'fumadocs-core/utils/use-effect-event';
const Context = createContext<{
open: boolean;
@@ -269,7 +268,7 @@ export function AISearchTrigger() {
}),
});
const onKeyPress = useEffectEvent((e: KeyboardEvent) => {
const onKeyPress = (e: KeyboardEvent) => {
if (e.key === 'Escape' && open) {
setOpen(false);
e.preventDefault();
@@ -279,12 +278,15 @@ export function AISearchTrigger() {
setOpen(true);
e.preventDefault();
}
});
};
const onKeyPressRef = useRef(onKeyPress);
onKeyPressRef.current = onKeyPress;
useEffect(() => {
window.addEventListener('keydown', onKeyPress);
return () => window.removeEventListener('keydown', onKeyPress);
}, [onKeyPress]);
const listener = (e: KeyboardEvent) => onKeyPressRef.current(e);
window.addEventListener('keydown', listener);
return () => window.removeEventListener('keydown', listener);
}, []);
return (
<Context value={useMemo(() => ({ chat, open, setOpen }), [chat, open])}>
+42 -33
View File
@@ -10,21 +10,23 @@ export interface BreadcrumbItem {
export interface BreadcrumbOptions {
/**
* Include the root itself in the breadcrumb items array.
* Specify the url by passing an object instead
* Include the root folders in the breadcrumb items array.
*
* @defaultValue false
*/
includeRoot?:
| boolean
| {
/**
* Specify the url of root
*/
url: string;
};
/**
* Include the page itself in the breadcrumb items array
*
* @defaultValue true
* @defaultValue false
*/
includePage?: boolean;
@@ -64,38 +66,45 @@ export function getBreadcrumbItemsFromPath(
path: PageTree.Node[],
options: BreadcrumbOptions,
): BreadcrumbItem[] {
const { includePage = true, includeSeparator = false, includeRoot } = options;
const {
includePage = false,
includeSeparator = false,
includeRoot = false,
} = options;
let items: BreadcrumbItem[] = [];
for (let i = 0; i < path.length; i++) {
const item = path[i];
path.forEach((item, i) => {
if (item.type === 'separator' && item.name && includeSeparator) {
items.push({
name: item.name,
});
switch (item.type) {
case 'page':
if (includePage)
items.push({
name: item.name,
url: item.url,
});
break;
case 'folder':
if (item.root && !includeRoot) {
items = [];
break;
}
// only show the index node of folders if possible
if (i === path.length - 1 || item.index !== path[i + 1]) {
items.push({
name: item.name,
url: item.index?.url,
});
}
break;
case 'separator':
if (item.name && includeSeparator)
items.push({
name: item.name,
});
break;
}
if (item.type === 'folder') {
const next = path.at(i + 1);
if (next && item.index === next) return;
if (item.root) {
items = [];
return;
}
items.push({
name: item.name,
url: item.index?.url,
});
}
if (item.type === 'page' && includePage) {
items.push({
name: item.name,
url: item.url,
});
}
});
}
if (includeRoot) {
items.unshift({
@@ -113,7 +122,7 @@ export function getBreadcrumbItemsFromPath(
* - When the page doesn't exist, return null
*
* @returns The path to the target node from root
* @internal
* @internal Don't use this on your own
*/
export function searchPath(
nodes: PageTree.Node[],
+3 -1
View File
@@ -1,5 +1,7 @@
/**
* normalize URL into the Fumadocs standard form (`/slug-1/slug-2`)
* normalize URL into the Fumadocs standard form (`/slug-1/slug-2`).
*
* This includes URLs with trailing slashes.
*/
export function normalizeUrl(url: string) {
if (url.startsWith('http://') || url.startsWith('https://')) return url;
+4 -11
View File
@@ -154,16 +154,9 @@ test('Breadcrumbs', () => {
],
};
expect(getBreadcrumbItems('/docs/folder', tree)).toStrictEqual([
{ name: 'World', url: '/docs/folder' },
]);
expect(
getBreadcrumbItems('/docs/folder', tree, { includePage: true }),
).toStrictEqual([{ name: 'World', url: '/docs/folder' }]);
expect(getBreadcrumbItems('/', tree)).toMatchInlineSnapshot(`
[
{
"name": "Introduction",
"url": "/",
},
]
`);
expect(getBreadcrumbItems('/invalid', tree)).toMatchInlineSnapshot(`[]`);
});
+2 -2
View File
@@ -340,9 +340,9 @@ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
export type BreadcrumbProps = BreadcrumbOptions & ComponentProps<'div'>;
export function PageBreadcrumb({
includeRoot = false,
includeRoot,
includeSeparator,
includePage = false,
includePage,
...props
}: BreadcrumbProps) {
const path = useTreePath();