Prepare search for upcoming records (#4037)

This commit is contained in:
Samy Pessé
2026-02-23 18:34:01 +01:00
committed by GitHub
parent 4962662fb1
commit a100c5a6c8
5 changed files with 144 additions and 9 deletions
@@ -32,17 +32,33 @@ async function handler(
);
return {
content: results.flatMap((spaceResult) => {
content: results.flatMap((result) => {
// @ts-expect-error - soon updated in the API
if (result.type === 'record') {
return {
type: 'text',
text: [
`Title: ${result.title}`,
// @ts-expect-error - soon updated in the API
`Link: ${result.href}`,
// @ts-expect-error - soon updated in the API
result.description ? `Content: ${result.description}` : '',
]
.filter(Boolean)
.join('\n'),
};
}
const found = findSiteSpaceBy(
context.structure,
(siteSpace) => siteSpace.space.id === spaceResult.id
(siteSpace) => siteSpace.space.id === result.id
);
const spaceURL = found?.siteSpace.urls.published;
if (!spaceURL) {
return [];
}
return spaceResult.pages.map((pageResult) => {
return result.pages.map((pageResult) => {
const pageURL = linker.toAbsoluteURL(
linker.toLinkForContent(
joinPathWithBaseURL(spaceURL, pageResult.path)
@@ -0,0 +1,79 @@
import { tString, useLanguage } from '@/intl/client';
import { Icon } from '@gitbook/icons';
import React from 'react';
import { HighlightQuery } from './HighlightQuery';
import { SearchResultItem } from './SearchResultItem';
import type { ComputedRecordResult } from './server-actions';
export const SearchRecordResultItem = React.forwardRef(function SearchRecordResultItem(
props: {
query: string;
item: ComputedRecordResult;
active: boolean;
},
ref: React.Ref<HTMLAnchorElement>
) {
const { query, item, active, ...rest } = props;
const language = useLanguage();
const domain = getDomain(item.href);
const faviconURL = domain ? getFaviconURL(domain) : null;
return (
<SearchResultItem
ref={ref}
href={item.href}
active={active}
data-testid="search-record-result"
action={tString(language, 'view')}
leadingIcon={
faviconURL ? (
<img src={faviconURL} alt="Favicon" className="size-4" />
) : (
<Icon icon="memo" className="size-4" />
)
}
// insights={{
// type: 'search_open_result',
// query,
// result: {
// pageId: item.pageId,
// spaceId: item.spaceId,
// },
// }}
aria-label={tString(language, 'search_page_result_title', item.title)}
{...rest}
>
<p className="line-clamp-2 font-semibold text-base text-tint-strong leading-snug">
<HighlightQuery query={query} text={item.title} />
</p>
{domain ? (
<p className="text-sm text-tint/7 group-[.is-active]:text-tint contrast-more:text-tint">
{domain}
</p>
) : null}
</SearchResultItem>
);
});
/**
* Get the domain from a URL.
*/
function getDomain(input: string) {
try {
const url = new URL(input);
return url.hostname;
} catch {
return null;
}
}
/**
* Use Google to get the favicon of a URL.
*/
function getFaviconURL(domain: string) {
const result = new URL('https://www.google.com/s2/favicons');
result.searchParams.set('domain', domain);
result.searchParams.set('sz', '64');
return result.toString();
}
@@ -10,6 +10,7 @@ import { tcls } from '@/lib/tailwind';
import { Button, Loading } from '../primitives';
import { SearchPageResultItem } from './SearchPageResultItem';
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
import { SearchRecordResultItem } from './SearchRecordResultItem';
import { SearchSectionResultItem } from './SearchSectionResultItem';
import type { OrderedComputedResult } from './server-actions';
@@ -207,6 +208,20 @@ export const SearchResults = React.forwardRef(function SearchResults(
/>
);
}
case 'record': {
return (
<SearchRecordResultItem
ref={(ref) => {
refs.current[index] = ref;
}}
key={item.id}
query={query}
item={item}
active={index === cursor}
{...resultItemProps}
/>
);
}
default:
assertNever(item);
}
@@ -57,7 +57,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe
);
});
function highlightQueryInBody(body: string, query: string) {
export function highlightQueryInBody(body: string, query: string) {
const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
// Ensure the query to be highlighted is visible in the body.
@@ -27,7 +27,10 @@ import { traceErrorOnly } from '@/lib/tracing';
import type { IconName } from '@gitbook/icons';
import { DocumentView } from '../DocumentView';
export type OrderedComputedResult = ComputedPageResult | ComputedSectionResult;
export type OrderedComputedResult =
| ComputedPageResult
| ComputedSectionResult
| ComputedRecordResult;
export interface ComputedSectionResult {
type: 'section';
@@ -53,6 +56,14 @@ export interface ComputedPageResult {
breadcrumbs?: Array<{ icon?: IconName; label: string }>;
}
export interface ComputedRecordResult {
type: 'record';
id: string;
title: string;
description: string;
href: string;
}
export interface AskAnswerSource {
id: string;
title: string;
@@ -238,19 +249,33 @@ export async function searchSiteContent({
return (
await Promise.all(
searchResults.map((spaceItem) => {
searchResults.map((resultItem) => {
// @ts-expect-error - will be added to the API soon
if (resultItem.type === 'record') {
const result: ComputedRecordResult = {
type: 'record',
id: resultItem.id,
title: resultItem.title,
// @ts-expect-error - will be added to the API soon
body: resultItem.description,
// @ts-expect-error - will be added to the API soon
href: resultItem.url,
};
return result;
}
const found = findSiteSpaceBy(
structure,
(siteSpace) => siteSpace.space.id === spaceItem.id
(siteSpace) => siteSpace.space.id === resultItem.id
);
const siteSection = found?.siteSection;
const siteSectionGroup = found?.siteSectionGroup;
return Promise.all(
spaceItem.pages.map((pageItem) =>
resultItem.pages.map((pageItem) =>
transformSitePageResult(context, {
pageItem,
spaceItem,
spaceItem: resultItem,
siteSpace: found?.siteSpace,
space: found?.siteSpace.space,
spaceURL: found?.siteSpace.urls.published,