Fuzzy Search
built with: fuse.js
A cmdk command palette driven by Fuse.js rankings, over an index aggregated from every content folder.
Turning off the filter you are given
cmdk ships its own matching and filtering. Fuse.js also ranks results. Letting both run means cmdk re-filters and re-orders a list Fuse already scored, discarding the fuzzy ranking that was the whole reason for adding Fuse.
So the palette sets `shouldFilter={false}` and takes full ownership of the result list. cmdk is used purely for what it is genuinely good at — the dialog, keyboard navigation, roving focus, and accessibility semantics — while relevance is decided entirely by Fuse.
// src/components/search/header-search.tsx
const fuse = useMemo(() => createSearchFuse(), []);
const results = useMemo(() => {
if (!value.trim()) return [];
return fuse
.search(value)
.slice(0, MAX_RESULTS)
.map((r) => r.item);
}, [fuse, value]);Building the index once
`src/data/search-index.ts` flattens notes, flashcards, machine-coding problems, custom hooks, and routes from every topic folder into one array of `SearchItem`s, each carrying a section label and a destination URL.
The Fuse instance is built inside `useMemo` with an empty dependency list, so the index is constructed once per mount rather than on every keystroke. Results are grouped back into their sections for display and capped at eight — a palette that fills the screen is harder to scan than one that does not.
Keyboard first
A document-level `keydown` listener binds Ctrl/Cmd-K to toggle the palette, calling `preventDefault` so the browser's own shortcut does not also fire. The listener is registered in an effect with a matching cleanup, so it is removed when the header unmounts.
Selecting a result routes with `router.push` and then clears both the open state and the query, so reopening the palette always starts from a blank slate rather than the previous search.
Important
Gotcha
Where it lives in the repo
- src/components/search/header-search.tsx
- src/data/search-index.ts
- src/components/ui/command.tsx