Skip to content

Commit c756761

Browse files
authored
Add breadcrumbs to search results (#2772)
1 parent 434af90 commit c756761

File tree

3 files changed

+80
-12
lines changed

3 files changed

+80
-12
lines changed

.changeset/giant-carrots-divide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Add breadcrumbs to search results

packages/gitbook/src/components/Search/SearchPageResultItem.tsx

+39-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Icon } from '@gitbook/icons';
1+
import { tcls } from '@/lib/tailwind';
2+
import { Icon, type IconName } from '@gitbook/icons';
23
import React from 'react';
34

4-
import { tcls } from '@/lib/tailwind';
55
import { Link } from '../primitives';
66
import { HighlightQuery } from './HighlightQuery';
77
import type { ComputedPageResult } from './server-actions';
@@ -16,6 +16,14 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
1616
) {
1717
const { query, item, active } = props;
1818

19+
const breadcrumbs =
20+
item.breadcrumbs?.map((crumb) => (
21+
<span key={crumb.label} className="flex items-center gap-1">
22+
{crumb.icon ? <Icon className="size-3" icon={crumb.icon as IconName} /> : null}
23+
{crumb.label}
24+
</span>
25+
)) ?? [];
26+
1927
return (
2028
<Link
2129
ref={ref}
@@ -53,18 +61,44 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
5361
/>
5462
</div>
5563
<div className={tcls('flex', 'flex-col', 'w-full')}>
56-
{item.spaceTitle ? (
64+
{breadcrumbs.length > 0 ? (
5765
<div
5866
className={tcls(
5967
'text-xs',
6068
'opacity-6',
69+
'contrast-more:opacity-11',
6170
'font-normal',
6271
'uppercase',
6372
'tracking-wider',
64-
'mb-1'
73+
'mb-1',
74+
'flex',
75+
'flex-wrap',
76+
'gap-x-2',
77+
'gap-y-1',
78+
'items-center'
6579
)}
6680
>
67-
{item.spaceTitle}
81+
{(breadcrumbs.length > 3
82+
? [
83+
...breadcrumbs.slice(0, 2),
84+
<Icon key="ellipsis" icon="ellipsis-h" className="size-3" />,
85+
...breadcrumbs.slice(-1),
86+
]
87+
: breadcrumbs
88+
).map((crumb, index) => (
89+
<>
90+
{index !== 0 ? (
91+
<Icon
92+
key={`${crumb.key}-icon`}
93+
icon="chevron-right"
94+
className="size-3"
95+
/>
96+
) : null}
97+
<span key={crumb.key} className="line-clamp-1">
98+
{crumb}
99+
</span>
100+
</>
101+
))}
68102
</div>
69103
) : null}
70104
<HighlightQuery query={query} text={item.title} />

packages/gitbook/src/components/Search/server-actions.tsx

+36-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use server';
22

33
import { resolvePageId } from '@/lib/pages';
4-
import { findSiteSpaceById } from '@/lib/sites';
4+
import { findSiteSpaceById, getSiteStructureSections } from '@/lib/sites';
55
import { filterOutNullable } from '@/lib/typescript';
66
import { getV1BaseContext } from '@/lib/v1';
77
import type {
@@ -10,6 +10,8 @@ import type {
1010
SearchAIRecommendedQuestionStream,
1111
SearchPageResult,
1212
SearchSpaceResult,
13+
SiteSection,
14+
SiteSectionGroup,
1315
Space,
1416
} from '@gitbook/api';
1517
import type { GitBookBaseContext, GitBookSiteContext } from '@v2/lib/context';
@@ -19,6 +21,7 @@ import type * as React from 'react';
1921

2022
import { joinPathWithBaseURL } from '@/lib/paths';
2123
import { isV2 } from '@/lib/v2';
24+
import type { IconName } from '@gitbook/icons';
2225
import { throwIfDataError } from '@v2/lib/data';
2326
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
2427
import { DocumentView } from '../DocumentView';
@@ -46,8 +49,7 @@ export interface ComputedPageResult {
4649
pageId: string;
4750
spaceId: string;
4851

49-
/** When part of a multi-spaces search, the title of the space */
50-
spaceTitle?: string;
52+
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
5153
}
5254

5355
export interface AskAnswerSource {
@@ -258,7 +260,16 @@ async function searchSiteContent(
258260
return (
259261
await Promise.all(
260262
searchResults.map(async (spaceItem) => {
263+
const sections = getSiteStructureSections(structure).flatMap((item) =>
264+
item.object === 'site-section-group' ? [item, ...item.sections] : item
265+
);
261266
const siteSpace = findSiteSpaceById(structure, spaceItem.id);
267+
const siteSection = sections.find(
268+
(section) => section.id === siteSpace?.section
269+
) as SiteSection;
270+
const siteSectionGroup = siteSection?.sectionGroup
271+
? sections.find((sectionGroup) => sectionGroup.id === siteSection.sectionGroup)
272+
: null;
262273

263274
return Promise.all(
264275
spaceItem.pages.map((pageItem) =>
@@ -267,6 +278,8 @@ async function searchSiteContent(
267278
spaceItem,
268279
space: siteSpace?.space,
269280
spaceURL: siteSpace?.urls.published,
281+
siteSection: siteSection ?? undefined,
282+
siteSectionGroup: (siteSectionGroup as SiteSectionGroup) ?? undefined,
270283
})
271284
)
272285
);
@@ -348,9 +361,11 @@ async function transformSitePageResult(
348361
spaceItem: SearchSpaceResult;
349362
space?: Space;
350363
spaceURL?: string;
364+
siteSection?: SiteSection;
365+
siteSectionGroup?: SiteSectionGroup;
351366
}
352367
): Promise<OrderedComputedResult[]> {
353-
const { pageItem, spaceItem, space, spaceURL } = args;
368+
const { pageItem, spaceItem, space, spaceURL, siteSection, siteSectionGroup } = args;
354369
const { linker } = context;
355370

356371
const page: ComputedPageResult = {
@@ -360,12 +375,26 @@ async function transformSitePageResult(
360375
href: spaceURL
361376
? linker.toLinkForContent(joinPathWithBaseURL(spaceURL, pageItem.path))
362377
: linker.toPathInSpace(pageItem.path),
363-
spaceTitle: space?.title,
364378
pageId: pageItem.id,
365379
spaceId: spaceItem.id,
380+
breadcrumbs: [
381+
siteSectionGroup && {
382+
icon: siteSectionGroup?.icon as IconName,
383+
label: siteSectionGroup.title,
384+
},
385+
siteSection && {
386+
icon: siteSection?.icon as IconName,
387+
label: siteSection.title,
388+
},
389+
(!siteSection || siteSection?.siteSpaces.length > 1) && space
390+
? {
391+
label: space?.title,
392+
}
393+
: undefined,
394+
].filter((item) => item !== undefined),
366395
};
367396

368-
const sections = await Promise.all(
397+
const pageSections = await Promise.all(
369398
pageItem.sections?.map<Promise<ComputedSectionResult>>(async (section) => ({
370399
type: 'section',
371400
id: `${page.id}/${section.id}`,
@@ -379,5 +408,5 @@ async function transformSitePageResult(
379408
})) ?? []
380409
);
381410

382-
return [page, ...sections];
411+
return [page, ...pageSections];
383412
}

0 commit comments

Comments
 (0)