diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/mcp/route.ts b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/mcp/route.ts index b327be776..25cf2dd2f 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/mcp/route.ts +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/mcp/route.ts @@ -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) diff --git a/packages/gitbook/src/components/Search/SearchRecordResultItem.tsx b/packages/gitbook/src/components/Search/SearchRecordResultItem.tsx new file mode 100644 index 000000000..7c85707eb --- /dev/null +++ b/packages/gitbook/src/components/Search/SearchRecordResultItem.tsx @@ -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 +) { + const { query, item, active, ...rest } = props; + const language = useLanguage(); + + const domain = getDomain(item.href); + const faviconURL = domain ? getFaviconURL(domain) : null; + + return ( + + ) : ( + + ) + } + // insights={{ + // type: 'search_open_result', + // query, + // result: { + // pageId: item.pageId, + // spaceId: item.spaceId, + // }, + // }} + aria-label={tString(language, 'search_page_result_title', item.title)} + {...rest} + > +

+ +

+ {domain ? ( +

+ {domain} +

+ ) : null} +
+ ); +}); + +/** + * 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(); +} diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx index fac209aba..cadcd62fb 100644 --- a/packages/gitbook/src/components/Search/SearchResults.tsx +++ b/packages/gitbook/src/components/Search/SearchResults.tsx @@ -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 ( + { + refs.current[index] = ref; + }} + key={item.id} + query={query} + item={item} + active={index === cursor} + {...resultItemProps} + /> + ); + } default: assertNever(item); } diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx index 95df9f16e..10df3ac09 100644 --- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx @@ -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. diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index cabb2abca..472bd6308 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -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,