Help Improvement 3 — Search Improvements
Status: [To Build]
Purpose: The current help search uses plain substring matching — “bolg” finds nothing, results show no context about why they matched, and there is no keyboard shortcut to reach the search box. Three targeted fixes address this.
Fix A — Fuzzy Search with Fuse.js
Replace the text.includes(query) filter with Fuse.js — a lightweight, zero-dependency fuzzy search library.
Why Fuse.js over alternatives
| Option | Size | Fuzzy | Ranking | Already in project |
|---|---|---|---|---|
String.includes (current) | 0 | No | No | Yes |
| Fuse.js | 24 kB | Yes | Yes | No — add once |
| Minisearch | 29 kB | Partial | Yes | No |
| Flexsearch | 40 kB | No | Yes | No |
Fuse.js gives fuzzy matching and result ranking with a single npm add fuse.js.
Implementation
// packages/ui/src/HelpCenter.tsx
import Fuse from "fuse.js";
// Build Fuse index once on mount
const fuse = useMemo(() => new Fuse(pages, {
keys: [
{ name: "title", weight: 0.4 },
{ name: "description", weight: 0.3 },
{ name: "tags", weight: 0.2 },
{ name: "categoryLabel", weight: 0.1 },
],
threshold: 0.35, // 0 = exact, 1 = match anything; 0.35 is forgiving but not noise
includeScore: true,
includeMatches: true, // needed for highlighting (Fix B)
}), [pages]);
// Replace the current filter:
const filtered = useMemo(() => {
const q = query.trim();
if (!q) return null;
return fuse.search(q).map(r => ({ page: r.item, matches: r.matches }));
}, [query, fuse]);Fix B — Match Highlighting in Results
Currently search results show the topic title and description with no indication of what matched. Highlighting the matching substrings makes results scannable.
HighlightText component
// packages/ui/src/HighlightText.tsx
interface HighlightTextProps {
text: string;
indices?: readonly [number, number][]; // Fuse.js match indices
className?: string;
}
export function HighlightText({ text, indices, className }: HighlightTextProps) {
if (!indices?.length) return <span className={className}>{text}</span>;
const parts: React.ReactNode[] = [];
let last = 0;
for (const [start, end] of indices) {
if (start > last) parts.push(text.slice(last, start));
parts.push(
<mark key={start} className="bg-yellow-100 dark:bg-yellow-900 text-inherit rounded-sm px-0.5">
{text.slice(start, end + 1)}
</mark>
);
last = end + 1;
}
if (last < text.length) parts.push(text.slice(last));
return <span className={className}>{parts}</span>;
}In the search result cards, replace plain {page.title} and {page.description} with:
<HighlightText
text={page.title}
indices={matches?.find(m => m.key === "title")?.indices}
/>
<HighlightText
text={page.description}
indices={matches?.find(m => m.key === "description")?.indices}
className="text-sm text-muted-foreground"
/>Fix C — Keyboard Focus Shortcut
When the user presses ? anywhere on the Help Center page, focus jumps to the search input. This is a common convention for search-heavy pages (GitHub, Linear, etc.).
Note: Ctrl+K is already taken by the global Typesense search modal — use ? (question mark) instead, which is natural for a help page.
// In HelpCenter component
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
function handleKey(e: KeyboardEvent) {
// Don't steal focus when typing in another input/textarea
if (e.key === "?" && e.target === document.body) {
e.preventDefault();
inputRef.current?.focus();
}
}
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, []);Add a subtle keyboard hint next to the search placeholder:
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
ref={inputRef}
placeholder="Search help topics…"
className="pl-9 pr-16 ..."
/>
<kbd className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground
border rounded px-1.5 py-0.5 font-mono hidden sm:inline">?</kbd>
</div>Affected Files
| File | Change |
|---|---|
packages/ui/package.json | Add fuse.js dependency |
packages/ui/src/HelpCenter.tsx | Replace substring filter with Fuse.js; pass match metadata to cards |
packages/ui/src/HighlightText.tsx | New component |
packages/ui/src/index.ts | Export HighlightText |