diff --git a/.changeset/giant-carrots-divide.md b/.changeset/giant-carrots-divide.md
new file mode 100644
index 000000000..def9ffb33
--- /dev/null
+++ b/.changeset/giant-carrots-divide.md
@@ -0,0 +1,5 @@
+---
+'gitbook': patch
+---
+
+Add breadcrumbs to search results
diff --git a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
index 501ac50be..6b4d0e1c0 100644
--- a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
+++ b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx
@@ -1,7 +1,7 @@
-import { Icon } from '@gitbook/icons';
+import { tcls } from '@/lib/tailwind';
+import { Icon, type IconName } from '@gitbook/icons';
import React from 'react';
-import { tcls } from '@/lib/tailwind';
import { Link } from '../primitives';
import { HighlightQuery } from './HighlightQuery';
import type { ComputedPageResult } from './server-actions';
@@ -16,6 +16,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
) {
const { query, item, active } = props;
+ const breadcrumbs =
+ item.breadcrumbs?.map((crumb) => (
+
+ {crumb.icon ? : null}
+ {crumb.label}
+
+ )) ?? [];
+
return (
- {item.spaceTitle ? (
+ {breadcrumbs.length > 0 ? (
- {item.spaceTitle}
+ {(breadcrumbs.length > 3
+ ? [
+ ...breadcrumbs.slice(0, 2),
+ ,
+ ...breadcrumbs.slice(-1),
+ ]
+ : breadcrumbs
+ ).map((crumb, index) => (
+ <>
+ {index !== 0 ? (
+
+ ) : null}
+
+ {crumb}
+
+ >
+ ))}
) : null}
diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx
index 1bc8ddf6b..b45bc0aba 100644
--- a/packages/gitbook/src/components/Search/server-actions.tsx
+++ b/packages/gitbook/src/components/Search/server-actions.tsx
@@ -1,7 +1,7 @@
'use server';
import { resolvePageId } from '@/lib/pages';
-import { findSiteSpaceById } from '@/lib/sites';
+import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import type {
@@ -10,6 +10,8 @@ import type {
SearchAIRecommendedQuestionStream,
SearchPageResult,
SearchSpaceResult,
+ SiteSection,
+ SiteSectionGroup,
Space,
} from '@gitbook/api';
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
@@ -19,6 +21,7 @@ import type * as React from 'react';
import { joinPathWithBaseURL } from '@/lib/paths';
import { isV2 } from '@/lib/v2';
+import type { IconName } from '@gitbook/icons';
import { throwIfDataError } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { DocumentView } from '../DocumentView';
@@ -46,8 +49,7 @@ export interface ComputedPageResult {
pageId: string;
spaceId: string;
- /** When part of a multi-spaces search, the title of the space */
- spaceTitle?: string;
+ breadcrumbs?: Array<{ icon?: IconName; label: string }>;
}
export interface AskAnswerSource {
@@ -258,7 +260,16 @@ async function searchSiteContent(
return (
await Promise.all(
searchResults.map(async (spaceItem) => {
+ const sections = getSiteStructureSections(structure).flatMap((item) =>
+ item.object === 'site-section-group' ? [item, ...item.sections] : item
+ );
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
+ const siteSection = sections.find(
+ (section) => section.id === siteSpace?.section
+ ) as SiteSection;
+ const siteSectionGroup = siteSection?.sectionGroup
+ ? sections.find((sectionGroup) => sectionGroup.id === siteSection.sectionGroup)
+ : null;
return Promise.all(
spaceItem.pages.map((pageItem) =>
@@ -267,6 +278,8 @@ async function searchSiteContent(
spaceItem,
space: siteSpace?.space,
spaceURL: siteSpace?.urls.published,
+ siteSection: siteSection ?? undefined,
+ siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
})
)
);
@@ -348,9 +361,11 @@ async function transformSitePageResult(
spaceItem: SearchSpaceResult;
space?: Space;
spaceURL?: string;
+ siteSection?: SiteSection;
+ siteSectionGroup?: SiteSectionGroup;
}
): Promise
{
- const { pageItem, spaceItem, space, spaceURL } = args;
+ const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
const { linker } = context;
const page: ComputedPageResult = {
@@ -360,12 +375,26 @@ async function transformSitePageResult(
href: spaceURL
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path))
: linker.toPathInSpace(pageItem.path),
- spaceTitle: space?.title,
pageId: pageItem.id,
spaceId: spaceItem.id,
+ breadcrumbs: [
+ siteSectionGroup && {
+ icon: siteSectionGroup?.icon as IconName,
+ label: siteSectionGroup.title,
+ },
+ siteSection && {
+ icon: siteSection?.icon as IconName,
+ label: siteSection.title,
+ },
+ (!siteSection || siteSection?.siteSpaces.length > 1) && space
+ ? {
+ label: space?.title,
+ }
+ : undefined,
+ ].filter((item) => item !== undefined),
};
- const sections = await Promise.all(
+ const pageSections = await Promise.all(
pageItem.sections?.map>(async (section) => ({
type: 'section',
id: `${page.id}/${section.id}`,
@@ -379,5 +408,5 @@ async function transformSitePageResult(
})) ?? []
);
- return [page, ...sections];
+ return [page, ...pageSections];
}