mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 18:43:29 +00:00
Prepare search for upcoming records (#4037)
This commit is contained in:
+19
-3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user