Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/gitbook/src/components/Search/SearchContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function SearchContainer(props: SearchContainerProps) {
const visible = viewport === 'desktop' ? !isMobile : viewport === 'mobile' ? isMobile : true;

const searchResultsId = `search-results-${React.useId()}`;
const { results, fetching } = useSearchResults({
const { results, fetching, error } = useSearchResults({
disabled: !(state?.query || withAI),
query: normalizedQuery,
siteSpaceId,
Expand Down Expand Up @@ -232,6 +232,7 @@ export function SearchContainer(props: SearchContainerProps) {
fetching={fetching}
results={results}
cursor={cursor}
error={error}
/>
) : null}
{showAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}
Expand Down
31 changes: 29 additions & 2 deletions packages/gitbook/src/components/Search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type Assistant, useAI } from '@/components/AI';
import { t, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';

import { Loading } from '../primitives';
import { Button, Loading } from '../primitives';
import { SearchPageResultItem } from './SearchPageResultItem';
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
import { SearchSectionResultItem } from './SearchSectionResultItem';
Expand Down Expand Up @@ -36,10 +36,11 @@ export const SearchResults = React.forwardRef(function SearchResults(
results: ResultType[];
fetching: boolean;
cursor: number | null;
error: boolean;
},
ref: React.Ref<SearchResultsRef>
) {
const { children, id, query, results, fetching, cursor } = props;
const { children, id, query, results, fetching, cursor, error } = props;

const language = useLanguage();

Expand Down Expand Up @@ -82,6 +83,32 @@ export const SearchResults = React.forwardRef(function SearchResults(
</div>
);
}
if (error) {
return (
<div
className={tcls(
'flex',
'flex-col',
'items-center',
'justify-center',
'text-center',
'py-8',
'h-full',
'gap-4'
)}
>
<div>{t(language, 'search_ask_error')}</div>
<Button
variant="secondary"
size="small"
// We do a reload because in case of a new deployment, the action might have changed and it requires a full reload to work again.
onClick={() => window.location.reload()}
>
{t(language, 'unexpected_error_retry')}
</Button>
</div>
);
}

const noResults = (
<div
Expand Down
88 changes: 52 additions & 36 deletions packages/gitbook/src/components/Search/useSearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export function useSearchResults(props: {
const [resultsState, setResultsState] = React.useState<{
results: ResultType[];
fetching: boolean;
}>({ results: [], fetching: false });
error: boolean;
}>({ results: [], fetching: false, error: false });

const { assistants } = useAI();
const withAI = assistants.length > 0;
Expand All @@ -54,7 +55,7 @@ export function useSearchResults(props: {
}
if (!query) {
if (!withAI) {
setResultsState({ results: [], fetching: false });
setResultsState({ results: [], fetching: false, error: false });
return;
}

Expand All @@ -64,11 +65,11 @@ export function useSearchResults(props: {
results,
`Cached recommended questions should be set for site-space ${siteSpaceId}`
);
setResultsState({ results, fetching: false });
setResultsState({ results, fetching: false, error: false });
return;
}

setResultsState({ results: [], fetching: false });
setResultsState({ results: [], fetching: false, error: false });

let cancelled = false;

Expand Down Expand Up @@ -102,7 +103,11 @@ export function useSearchResults(props: {
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);

if (!cancelled) {
setResultsState({ results: [...recommendedQuestions], fetching: false });
setResultsState({
results: [...recommendedQuestions],
fetching: false,
error: false,
});
}
}
}, 100);
Expand All @@ -112,44 +117,55 @@ export function useSearchResults(props: {
clearTimeout(timeout);
};
}
setResultsState((prev) => ({ results: prev.results, fetching: true }));
setResultsState((prev) => ({ results: prev.results, fetching: true, error: false }));
let cancelled = false;
const timeout = setTimeout(async () => {
const results = await (() => {
if (scope === 'all') {
// Search all content on the site
return searchAllSiteContent(query);
}
if (scope === 'default') {
// Search the current section's variant + matched/default variant for other sections
return searchCurrentSiteSpaceContent(query, siteSpaceId);
}
if (scope === 'extended') {
// Search all variants of the current section
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
try {
const results = await (() => {
if (scope === 'all') {
// Search all content on the site
return searchAllSiteContent(query);
}
if (scope === 'default') {
// Search the current section's variant + matched/default variant for other sections
return searchCurrentSiteSpaceContent(query, siteSpaceId);
}
if (scope === 'extended') {
// Search all variants of the current section
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
}
if (scope === 'current') {
// Search only the current section's current variant
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
}
throw new Error(`Unhandled search scope: ${scope}`);
})();

if (cancelled) {
return;
}
if (scope === 'current') {
// Search only the current section's current variant
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);

if (!results) {
// One time when this one returns undefined is when it cannot find the server action and returns the html from the page.
// In that case, we want to avoid being stuck in a loading state, but it is an error.
// We could potentially try to force reload the page here, but i'm not 100% sure it would be a better experience.
setResultsState({ results: [], fetching: false, error: true });
return;
}
throw new Error(`Unhandled search scope: ${scope}`);
})();

if (cancelled) {
return;
}
setResultsState({ results, fetching: false, error: false });

if (!results) {
setResultsState({ results: [], fetching: false });
return;
trackEvent({
type: 'search_type_query',
query,
});
} catch {
// If there is an error, we need to catch it to avoid infinite loading state.
if (cancelled) {
return;
}
setResultsState({ results: [], fetching: false, error: true });
}

setResultsState({ results, fetching: false });

trackEvent({
type: 'search_type_query',
query,
});
}, 350);

return () => {
Expand Down
18 changes: 16 additions & 2 deletions packages/gitbook/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,20 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// Handle redirects
//
if ('redirect' in siteURLData) {
// When it is a server action, we cannot just return a redirect response as it may cause CORS issues on redirect.
// For these cases, we return a 303 response with an `X-Action-Redirect` header that the client can handle.
// This is what server actions do when returning a redirect response.
const isServerAction = request.headers.has('next-action') && request.method === 'POST';
const createRedirectResponse = (url: string) =>
isServerAction
? new NextResponse(null, {
status: 303,
headers: {
'X-Action-Redirect': `${url};push`,
'Content-Security-Policy': getContentSecurityPolicy(),
},
})
: NextResponse.redirect(url);
// biome-ignore lint/suspicious/noConsole: we want to log the redirect
console.log('redirect', siteURLData.redirect);
if (siteURLData.target === 'content') {
Expand All @@ -221,10 +235,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// as it might contain a VA token
contentRedirect.search = request.nextUrl.search;

return NextResponse.redirect(contentRedirect);
return createRedirectResponse(contentRedirect.toString());
}

return NextResponse.redirect(siteURLData.redirect);
return createRedirectResponse(siteURLData.redirect);
}

cookies.push(
Expand Down