Skip to content
← All posts
5 min read

Adding search to /tools, /blog, and /news without a backend

No database, no API route, no server: just Fuse.js running in the browser against data we already had.

Fuzzy search across every tool, post, and story, running entirely in the browser with zero backend.

This site is a static export: no server, no API route, no database. So when we wanted quick search on /tools, /blog, and /news, the usual 'call an endpoint' playbook was off the table. Here is what we looked at, what we picked, and what it actually took to wire up three pages worth maybe fifty items total.

The constraint: no backend, ever

`next.config.mjs` sets `output: 'export'`. Every page here is pre-rendered HTML served straight from Cloudflare Pages, no server-side code runs at request time. That rules out anything that expects an API route or a database to query, which is most of what 'add search to your site' articles assume.

What we looked at

Four real options came up: Pagefind, a static search library that indexes your built HTML after the fact and is built for exactly this kind of site; FlexSearch, the fastest of the bunch, tuned for datasets in the hundreds of thousands of documents; MiniSearch, a solid all-rounder for small to medium static sites; and Fuse.js, a fuzzy matcher that tolerates typos better than the others.

Pagefind is the right tool once a site has hundreds of pages: it runs as a postbuild step that crawls your `out/` directory and writes its own index files. We have maybe fifty posts, tools, and news items combined right now. Standing up a build-time indexing step for that is more plumbing than the problem needs. FlexSearch's whole pitch is raw speed at 100k+ documents, also not our problem.

What we picked, and why

Fuse.js. The dataset already lives in memory as plain TypeScript arrays (`posts.ts`, `news.ts`, `tools.ts`), so there is no index to build, no postbuild step, no JSON file to fetch. We import the array, hand it to `new Fuse(items, { keys: [...] })` inside a small client component, and filter on every keystroke. It runs about 12KB gzipped, has no dependency on Node APIs, and its fuzzy matching means a search for 'selfhosted' still finds 'selfhost.directory' even with the space missing.

The tradeoff we accepted: Fuse.js keeps the whole list in the browser and scores every item on every keystroke. Fine at our scale, the kind of thing that would need a rethink well before we hit even a few hundred items.

How it is wired up

One shared `SearchBox` component handles the input and the 'no results' copy. Each listing page got a small client component, `BlogList`, `NewsList`, `ToolsList`, that takes the existing data as props, keeps the search string in `useState`, and runs it through a `useMemo`'d Fuse instance. The page itself stays a server component, so the metadata, JSON-LD, and the rest of the layout are unaffected. Only the grid of cards became interactive.

const fuse = useMemo(
  () => new Fuse(posts, { keys: ["title", "description", "tags"], threshold: 0.35 }),
  [posts]
);
const results = query.trim() ? fuse.search(query).map((r) => r.item) : posts;

What it will not do

Search only matches what is already in the data: title, description, and tags. It will not find a phrase buried three paragraphs into a post body, we did not index full post content, mainly to keep the client bundle light. If that becomes a real gap, Pagefind's static indexing is the natural next step once there is enough content to justify it.

Three pages, one small shared component, one dependency. The right search stack is the smallest one that is still honest about your dataset size, not the one with the most features.

Next step: try it on /tools, /blog, or /news. Type something with a typo in it, that is the part Fuse.js earns its keep on.

EngineeringSearchNext.jsStaticSiteNextJSClientSideSearchFuseJSWebDevJAMstackOpenSourceSearchUXFrontendEngineeringReact
04 · BRIEF

Ready to ship?

Send a brief. You will get a written reply, a fixed quote, and a delivery date within 24 hours. If we miss that window, the website fee on your first project is refunded in full.

24h reply, or 100% website fee refunded
Average reply under 4 hours. Always inside 24.