Compare commits

...

2 Commits

Author SHA1 Message Date
Claude 68a881808f changeset 2026-07-15 00:17:46 +00:00
Claude 378f519231 fix(search): link title matches to page top instead of section anchor
Search page results always linked to the best-scoring section's anchor
whenever a snippet was shown, so clicking a result whose page title
matched the query would drop the user to a heading partway down the
article instead of the top.

Link to the top of the page when the query is a page/title match (every
query word appears in the title) and keep the section anchor only for
genuine section-level matches. Adds a pure `isPageTitleMatch` helper with
unit tests, mirroring the per-word title matching already used for scoring
in reciprocalRankFusion.

RND-11916

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xihqNNfTuEzwsux5wzMoF
2026-07-15 00:17:44 +00:00
4 changed files with 64 additions and 3 deletions
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Search: link page/title matches to the top of the page instead of a 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 for genuine section matches. On a page/title
// match, go to the top of the page even when a section snippet is shown, so
// clicking a title result lands where the user expects.
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,32 @@
import { describe, expect, it } from 'bun:test';
import { isPageTitleMatch } from './isPageTitleMatch';
describe('isPageTitleMatch', () => {
it('matches when the query equals the title', () => {
expect(isPageTitleMatch('EDR Integrations', 'EDR Integrations')).toBe(true);
});
it('is case-insensitive and tolerant of extra whitespace', () => {
expect(isPageTitleMatch(' edr integrations ', 'EDR Integrations')).toBe(true);
});
it('matches when word order differs', () => {
expect(isPageTitleMatch('integrations edr', 'EDR Integrations')).toBe(true);
});
it('matches a single title word', () => {
expect(isPageTitleMatch('integrations', 'EDR Integrations')).toBe(true);
});
it('does not match a section-level query whose words are absent from the title', () => {
expect(isPageTitleMatch('selecting crowdstrike URL', 'EDR Integrations')).toBe(false);
});
it('does not match when only some query words are in the title', () => {
expect(isPageTitleMatch('EDR onboarding guide', 'EDR Integrations')).toBe(false);
});
it('returns false for an empty query', () => {
expect(isPageTitleMatch(' ', 'EDR Integrations')).toBe(false);
});
});
@@ -0,0 +1,20 @@
/**
* Whether the query targets the page itself (its title) rather than a specific
* section within it. True when every word of the query appears in the page
* title. We treat that as a page/title match so the result links to the top of
* the page instead of dropping to a section anchor. Mirrors the per-word title
* matching used for scoring in `reciprocalRankFusion`.
*/
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 normalizedTitle = title.toLowerCase();
return queryWords.every((word) => normalizedTitle.includes(word));
}