Adding Full-Text Search to Nuxt Content Pages with Fuse.js
AI generated
{ }
Nuxt Content · Search · Fuse.js
Full-text search in Nuxt Content with Fuse.js
Client-side fuzzy search without an external search service

Fuse.js is a lightweight JavaScript library for client-side fuzzy search that pairs well with the statically generated content index of Nuxt Content, without needing to run or pay for an external search service such as Algolia. For a project with a manageable number of content pages, it makes a typo-tolerant search possible entirely in the browser, including weighting titles higher than body text, with no additional server infrastructure required.

14 min read Fuse.js · Fuzzy search Nuxt Content search

1. Why Fuse.js fits client-side search in Nuxt Content

Fuse.js implements fuzzy search logic that still surfaces relevant results even with slightly different spelling, typos, or incomplete search terms, unlike a simple substring search that only recognizes exact partial matches. The entire search runs inside the user's browser without sending a request to any server, which fits statically generated Nuxt Content pages particularly well, since the full content is already known at build time anyway.

This combination works best for projects with a manageable, but not trivial, number of pages, such as a company blog, documentation, or a knowledge base with a few hundred entries. For projects that size, running a dedicated search service is rarely worth it, while a plain substring search quickly hits its limits once users do not type the exact term that appears in the text.

2. Building a search index from the generated content

The first step is generating a flat JSON array from every Nuxt Content page, containing exactly the fields that should later be searchable, typically a title, a short description, and an excerpt from the body text. This index can either be generated at build time through a dedicated Nitro plugin or fetched directly through Nuxt Content's query API and cached on the client.

It matters to keep the index deliberately lean and avoid stuffing the full Markdown content of every page into it, since this index is shipped in full to the client and its size directly affects how quickly the search feature becomes usable. An excerpt of a few hundred characters from the body text is usually enough to surface relevant results without needlessly bloating the index.


// composables/useSearchIndex.ts
import Fuse from 'fuse.js'

interface SearchEntry {
  title: string
  description: string
  excerpt: string
  path: string
}

export function useSearchIndex() {
  const { data: entries } = useAsyncData<SearchEntry[]>('search-index', () =>
    queryContent('/blog').only(['title', 'description', 'excerpt', '_path']).find()
  )

  const fuse = computed(() => new Fuse(entries.value ?? [], {
    keys: [
      { name: 'title', weight: 0.6 },
      { name: 'description', weight: 0.3 },
      { name: 'excerpt', weight: 0.1 },
    ],
    threshold: 0.35,
    ignoreLocation: true,
  }))

  return { fuse }
}

3. Field weighting: prioritizing titles over body text

Fuse.js lets every searched field be assigned its own weight between 0 and 1 through the keys configuration, with higher values counting more heavily toward the computed relevance score. In practice, weighting the title noticeably higher than the description and the body excerpt tends to work well, because a match in the title is usually more topically relevant than a random occurrence of a search term somewhere in the running text.

A typical split might use 0.6 for the title, 0.3 for the description, and 0.1 for the body excerpt, though the exact values are best calibrated by trying real search queries from your own audience. Without deliberate weighting, Fuse.js treats every field equally, which can let a random match buried in body text outrank a much more relevant title match in the results list.

4. Tuning threshold and fuzzy tolerance correctly

The threshold parameter controls how tolerant Fuse.js is toward differences between the search term and a match, ranging from 0 (exact match only) to 1 (nearly anything matches). A value set too low returns almost nothing when there is a typo, while a value set too high floods the result list with barely relevant matches, which makes the search feel confusing rather than helpful to users.

A starting value between 0.3 and 0.4 has proven to be a reasonable compromise across many projects, but it should be verified against real user queries once enough data is available. The ignoreLocation option additionally helps surface matches regardless of where in the text the search term appears, which matters particularly for longer body-text excerpts.

5. Keeping an eye on performance as the page count grows

As long as the search index covers a few hundred to a couple thousand entries, Fuse.js stays comfortably fast even on average hardware, since the fuzzy search runs entirely in the browser's memory. As the entry count grows, though, both the time needed to build the Fuse index and the time needed for each individual search increase, because Fuse.js internally scores every entry against the search term.

Beyond a few tens of thousands of entries, this becomes noticeable, especially on lower-end mobile devices, where a delay of several hundred milliseconds per keystroke already feels sluggish. At that scale, it is worth either adding a debounce strategy that only triggers the search after a brief pause in typing, or moving to a server-side search solution that no longer needs to compute results in the client.

6. Debouncing and result presentation for a smooth experience

Without debouncing, every single keystroke would trigger a new Fuse.js search, which can produce noticeable input lag on larger indexes because the search briefly blocks the browser's main thread. A delay of 150 to 250 milliseconds between the last keystroke and the actual search execution is usually enough to avoid that effect without making the search feel slow to users.

When rendering results, it also helps to sort matches by the relevance score Fuse.js already provides by default, and optionally to highlight the matched portion of the text, so users immediately understand why a given result showed up. These small details often matter more to perceived search quality than the exact fuzzy logic running underneath.

7. An alternative to an external search service such as Algolia

Algolia and similar hosted search services offer considerably more sophisticated relevance algorithms, typo-tolerant search backed by machine learning, faceted filtering, and practically unlimited scalability, but they come with ongoing costs and an additional external dependency that has to be maintained and kept in sync separately. For smaller to mid-sized projects, that extra effort often does not pay off relative to the actual benefit.

Fuse.js, by contrast, costs nothing, needs no external service, and runs entirely within the existing Nuxt deployment, which considerably simplifies both maintenance and privacy, since no search queries are ever sent to a third party. The trade-off lies in more limited scalability and simpler relevance algorithms, which for most content sites in the low to mid four-digit entry range is not a practical constraint at all.

If a site runs in multiple languages, the search index should be built separately per language, since a shared fuzzy search across content in different languages rarely produces meaningful results and users usually only want to search within their current language version anyway. In practice, that means either generating the index separately per language or filtering it by the current locale before handing it to Fuse.js.

This separation also prevents a German search term from accidentally matching an English content entry with a similar character sequence, which can happen without a clean split, since Fuse.js optimizes purely for character similarity and has no notion of linguistic meaning. Thinking about language separation from the start saves a considerable amount of rework to the search logic later on.

9. Keeping the search index current with new or changed content

Since the search index is typically generated at build time from the current content, it automatically reflects the state of the last deployment without needing any separate synchronization step, unlike what is often required with an external search service maintaining its own index. After every new build, the updated index becomes available as soon as the site is redeployed.

For projects with very frequent content changes outside of regular deployments, for example through a connected CMS with instant publishing, it can make sense to additionally regenerate the index at runtime through a dedicated API route, instead of only relying on the next build. That decision depends heavily on how time-critical it is for new content to become findable through search.

Criterion Fuse.js (client-side) Algolia (hosted) Plain substring search
Cost Free, no service needed Ongoing usage costs Free
Typo tolerance Yes, configurable fuzzy logic Yes, very mature No, exact partial matches only
Scalability Good up to a few thousand entries Practically unlimited Good, but not very relevant
External dependency None, runs client-side Yes, hosted third-party service None
Setup effort Low to moderate Moderate to high Very low

Mironsoft

Vue architecture, Composition API, and Nuxt performance

Vue applications that don't get more complicated with every feature?

We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.

Architecture Review

Checking composables, state management, and component structure for maintainability.

Performance Audit

Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.

Nuxt Integration

Building robust, type-safe SSR/SSG setup and API integration.

10. Summary

Fuse.js search in Nuxt Content: the essentials at a glance

Core idea

Fuzzy search running entirely in the browser over a build-time generated content index.

Weighting

Weight the title considerably higher than the description and body-text excerpt.

Limit

Beyond a few tens of thousands of entries, a server-side solution becomes more sensible.

Alternative

Algolia offers more scalability, but with ongoing costs and an external dependency.

11. FAQ: Fuse.js search in Nuxt Content: the essentials at a glance

1Do I need to run my own backend service for Fuse.js?
No, Fuse.js runs entirely in the user's browser. The search index is either generated as JSON at build time or loaded through the existing Nuxt Content query API; a separate search server is not required.
2How large can the search index get before Fuse.js slows down?
Up to a few thousand entries, Fuse.js stays comfortably fast on average hardware. Beyond roughly ten thousand entries, search time per keystroke becomes noticeable, especially on weaker mobile devices.
3Can Fuse.js search for multiple terms at once?
Yes, Fuse.js supports an extended search syntax with logical AND and OR operators, which can be enabled through the useExtendedSearch option if simple fuzzy search is not enough.
4How does Fuse.js differ from a plain JavaScript filter function?
A plain filter function only finds exact substrings, while Fuse.js tolerates typos and spelling variations and additionally sorts results by a computed relevance score.
5How should I build the search index for a very large blog with thousands of articles?
In that case it is worth keeping the index deliberately lean, including only the most important fields, and possibly adding pagination or category pre-filtering before Fuse.js searches over the remaining subset.
6Can Fuse.js also search images or other non-text content?
No, Fuse.js works exclusively with text fields. Images would need to be made indirectly searchable through accompanying text, such as alt text or captions.
7How do I handle special characters or accented letters in search terms?
Fuse.js handles Unicode characters, including accented letters, correctly by default. If needed, an additional normalization step, such as unifying letter case, can be applied before the index is built.
8At what project size does switching to Algolia become worth it?
A switch usually only pays off once the entry count reaches the tens of thousands, faceted filtering across many attributes is needed, or a search team with dedicated relevance tuning is available.
9Can I use Fuse.js for pure product search instead of blog content?
Yes, Fuse.js is content-agnostic and works just as well for product data, as long as it is available as a flat JSON array with the relevant searchable fields.
10Does a large Fuse.js search affect the page's Core Web Vitals?
The search index is usually only loaded once a user interacts with the search, so it barely affects initial load times. If the index is instead loaded unfiltered on first page load, it can noticeably degrade interactivity metrics.