fix: improve search to find terms in page headings and body content

Karen Li committed Apr 10, 2026 at 21:30 UTC 7b91f4b896f51852adacf2e58dfec3ec019cf7dd
3 files changed +66 -1
src/hooks/__tests__/use-search.test.js new
+47
@@ -0,0 +1,47 @@
1 +import {flattenHeadings} from '../use-search'
2 +
3 +describe('flattenHeadings', () => {
4 + test('returns empty array for null/undefined input', () => {
5 + expect(flattenHeadings(null)).toEqual([])
6 + expect(flattenHeadings(undefined)).toEqual([])
7 + })
8 +
9 + test('returns empty array for empty items', () => {
10 + expect(flattenHeadings([])).toEqual([])
11 + })
12 +
13 + test('flattens single level of headings', () => {
14 + const items = [{title: 'Description'}, {title: 'Configuration'}]
15 + expect(flattenHeadings(items)).toEqual(['Description', 'Configuration'])
16 + })
17 +
18 + test('flattens nested headings', () => {
19 + const items = [
20 + {
21 + title: 'Description',
22 + items: [{title: 'before'}, {title: 'min-release-age'}],
23 + },
24 + ]
25 + expect(flattenHeadings(items)).toEqual(['Description', 'before', 'min-release-age'])
26 + })
27 +
28 + test('flattens deeply nested headings', () => {
29 + const items = [
30 + {
31 + title: 'Top',
32 + items: [
33 + {
34 + title: 'Mid',
35 + items: [{title: 'Deep'}],
36 + },
37 + ],
38 + },
39 + ]
40 + expect(flattenHeadings(items)).toEqual(['Top', 'Mid', 'Deep'])
41 + })
42 +
43 + test('skips items without a title', () => {
44 + const items = [{items: [{title: 'child'}]}, {title: 'sibling'}]
45 + expect(flattenHeadings(items)).toEqual(['child', 'sibling'])
46 + })
47 +})
src/hooks/use-search.js
+11
@@ -6,6 +6,15 @@ import usePage from './use-page'
6 import * as getNav from '../util/get-nav'
7 import {CLI_PATH} from '../constants'
8
9 +export const flattenHeadings = items => {
10 + if (!items) return []
11 + return items.reduce((acc, item) => {
12 + if (item.title) acc.push(item.title)
13 + if (item.items) acc.push(...flattenHeadings(item.items))
14 + return acc
15 + }, [])
16 +}
17 +
18 const useSearchData = () => {
19 const data = useStaticQuery(graphql`
20 {
@@ -15,6 +24,7 @@ const useSearchData = () => {
24 frontmatter {
25 title
26 }
27 + tableOfContents
28 body
29 }
30 }
@@ -40,6 +50,7 @@ const useSearchData = () => {
50 return {
51 path: node.path,
52 title: mdxNode.frontmatter.title,
53 + headings: flattenHeadings(mdxNode.tableOfContents?.items).join(' '),
54 body: mdxNode.body,
55 }
56 })
src/util/search.worker.js
+8 -1
@@ -36,7 +36,14 @@ class Fuse {
36 const items = this.allItems.filter(item =>
37 item.path.startsWith(this.cliVersion.root) ? item.path.startsWith(this.cliVersion.current) : true,
38 )
39 - this.instances.set(this.cliVersion.current, new FuseJs(items, {threshold: 0.2, keys: ['title', 'body']}))
39 + this.instances.set(
40 + this.cliVersion.current,
41 + new FuseJs(items, {
42 + threshold: 0.2,
43 + ignoreLocation: true,
44 + keys: ['title', 'headings', 'body'],
45 + }),
46 + )
47 }
48
49 setItems(items) {