Fix search results linking to section anchors on title matches

Search results that match a page title now link to the top of the page
instead of a mid-page section anchor. Section-level matches keep their
heading anchor.

Adds a pure `isPageTitleMatch` helper (mirroring the substring title
notion in reciprocalRankFusion) and uses it in SearchPageResultItem to
choose the link target. Recovers the approach from the previously-closed
draft PR #4434 for RND-11916.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1rctbY7mEtGP1ZwVowxoE
This commit is contained in:
Claude
2026-07-29 00:14:48 +00:00
parent 5c74eef602
commit 32ef4f6aed
4 changed files with 76 additions and 3 deletions
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Search results that match a page title now link to the top of the page instead of a mid-page section anchor; section-level matches keep their heading anchor.
@@ -7,6 +7,7 @@ import { Tooltip } from '../primitives';
import { Emoji } from '../primitives/Emoji/Emoji';
import { HighlightQuery } from './HighlightQuery';
import { SearchResultItem } from './SearchResultItem';
import { isPageTitleMatch } from './isPageTitleMatch';
import type { MergedPageResult } from './reciprocalRankFusion';
import type { ComputedPageResult } from './search-types';
import type { LocalPageResult } from './useLocalSearchResults';
@@ -26,9 +27,12 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt
const language = useLanguage();
const bestSection = item.type === 'page' ? item.bestSection : undefined;
// When the section snippet is displayed, link to the section anchor so the
// link matches what the user sees.
const href = bestSection?.body ? bestSection.href : 'href' in item ? item.href : item.pathname;
const pageHref = 'href' in item ? item.href : item.pathname;
// Link to the section anchor only when a section snippet is shown AND the
// query is not a title match. Title/page matches link to the top of the page;
// section-level matches keep their heading anchor.
const href =
bestSection?.body && !isPageTitleMatch(query, item.title) ? bestSection.href : pageHref;
const emoji = 'emoji' in item ? item.emoji : undefined;
const icon = 'icon' in item ? item.icon : undefined;
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'bun:test';
import { isPageTitleMatch } from './isPageTitleMatch';
describe('isPageTitleMatch', () => {
it('matches when every query word is a substring of the title', () => {
expect(isPageTitleMatch('EDR Integrations', 'EDR Integrations')).toBe(true);
});
it('is case-insensitive', () => {
expect(isPageTitleMatch('edr integrations', 'EDR Integrations')).toBe(true);
});
it('matches when the title contains the query as part of a longer title', () => {
expect(isPageTitleMatch('EDR', 'EDR Integrations')).toBe(true);
});
it('does not match a section-level query whose words are not all in the title', () => {
expect(isPageTitleMatch('selecting crowdstrike URL', 'EDR Integrations')).toBe(false);
});
it('requires every query word to appear in the title', () => {
expect(isPageTitleMatch('EDR missing', 'EDR Integrations')).toBe(false);
});
it('returns false for an empty query', () => {
expect(isPageTitleMatch('', 'EDR Integrations')).toBe(false);
});
it('returns false for a whitespace-only query', () => {
expect(isPageTitleMatch(' ', 'EDR Integrations')).toBe(false);
});
it('ignores extra whitespace between query words', () => {
expect(isPageTitleMatch(' EDR Integrations ', 'EDR Integrations')).toBe(true);
});
it('matches substrings within a title word (substring heuristic)', () => {
expect(isPageTitleMatch('api', 'Rapid start')).toBe(true);
});
});
@@ -0,0 +1,24 @@
/**
* Whether a search query matches a page by its title.
*
* There is no server-provided title-vs-section flag on search results, so we
* infer a title match the same way title scoring does in `reciprocalRankFusion`:
* a match when every whitespace-split query word appears (case-insensitively)
* as a substring of the title.
*
* Used to decide whether a result should link to the top of the page (title
* match) or keep its section anchor (section-level match).
*/
export function isPageTitleMatch(query: string, title: string): boolean {
const queryWords = query
.toLowerCase()
.split(/\s+/)
.filter((word) => word.length > 0);
if (queryWords.length === 0) {
return false;
}
const lowerTitle = title.toLowerCase();
return queryWords.every((word) => lowerTitle.includes(word));
}