This content originally appeared on Telerik Blogs and was authored by Jonathan Gamble
Learn how to work with fuzzy search, scoring and multiple fields and allow your Nuxt app to search static data.
Do you ever see documentation websites that have a search feature and it’s super fast? Well, the novices decide to use a real database like Algolia and potentially pay for it. The pros use pure JavaScript.
TL;DR
Learn to create a Nuxt application that can search static data. Set the data as an object, then filter through the results in real time to get the desired page. This demo adds fuzzy search, uses scoring and handles multiple fields.
Nuxt
This demo uses Nuxt and assumes you have a basic understanding of composables, pages and layouts.
Layout
Create the default layout.
// layouts/default.vue
<template>
<main class="min-h-screen bg-gray-100 text-gray-800">
<div class="flex flex-col items-center py-8">
<img src="https://www.telerik.com/bttf.webp" alt="Back to the Future man!" />
</div>
<div class="max-w-4xl mx-auto px-6">
<slot />
</div>
<nav class="bg-white border-t border-gray-200 mt-12 py-8">
<div class="max-w-4xl mx-auto px-6">
<h2 class="text-xl font-bold text-blue-700 mb-4">
Back to the Future Archive
</h2>
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<li>
<NuxtLink to="/" class="hover:underline text-blue-600">
Home
</NuxtLink>
</li>
<li>
<NuxtLink
to="/vehicles/delorean"
class="hover:underline text-blue-600"
>
The DeLorean Time Machine
</NuxtLink>
</li>
<li>
<NuxtLink
to="/characters/marty"
class="hover:underline text-blue-600"
>
Marty McFly
</NuxtLink>
</li>
<li>
<NuxtLink
to="/characters/doc-brown"
class="hover:underline text-blue-600"
>
Doc Emmett Brown
</NuxtLink>
</li>
<li>
<NuxtLink
to="/timeline/hill-valley"
class="hover:underline text-blue-600"
>
Hill Valley Timeline
</NuxtLink>
</li>
<li>
<NuxtLink
to="/tech/hoverboard"
class="hover:underline text-blue-600"
>
Hoverboard Technology
</NuxtLink>
</li>
<li>
<NuxtLink
to="/characters/biff"
class="hover:underline text-blue-600"
>
Biff Tannen's Antagonism
</NuxtLink>
</li>
<li>
<NuxtLink
to="/tech/flux-capacitor"
class="hover:underline text-blue-600"
>
Flux Capacitor
</NuxtLink>
</li>
<li>
<NuxtLink
to="/timeline/1985-vs-2015"
class="hover:underline text-blue-600"
>
1985 vs. 2015
</NuxtLink>
</li>
<li>
<NuxtLink
to="/events/enchantment-dance"
class="hover:underline text-blue-600"
>
Enchantment Under the Sea Dance
</NuxtLink>
</li>
<li>
<NuxtLink
to="/shop/merchandise"
class="hover:underline text-blue-600"
>
Back to the Future Merchandise
</NuxtLink>
</li>
</ul>
</div>
</nav>
</main>
</template>
This is just some links to Back to the Future pages.
Create Catch-All Route
I didn’t feel like manually creating a bunch of pages, so I just generated them. In a real world app, you will probably have generated static files created in markdown. If you generate pages from a database, you would probably use the database itself to search.
// pages/[...slug].vue
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
const fullPath = "/" + (route.params.slug as string[]).join("/");
const page = items.find((item) => item.url === fullPath);
</script>
<template>
<div v-if="page">
<h1>{{ page.title }}</h1>
<p>{{ page.description }}</p>
</div>
<div v-else>
<h1>404 - Page Not Found</h1>
<p>The requested page does not exist.</p>
</div>
</template>
Again, this is for demo purposes only.
Searching: The Actual Data
We want to store our data in a large object.
// composables/useSearch.ts
type BTTFItem = {
title: string;
description: string;
url: string;
};
export const items: BTTFItem[] = [
{
title: "The DeLorean Time Machine",
description: "Explore the iconic DeLorean, the time-traveling car built by Doc Brown.",
url: "/vehicles/delorean"
},
{
title: "Marty McFly",
description: "Learn all about the skateboarding teenager who travels through time.",
url: "/characters/marty"
},
{
title: "Doc Emmett Brown",
description: "Meet the eccentric inventor behind the time machine.",
url: "/characters/doc-brown"
},
{
title: "Hill Valley Timeline",
description: "A deep dive into the changing history of Hill Valley across the trilogy.",
url: "/timeline/hill-valley"
},
{
title: "Hoverboard Technology",
description: "Discover the future of personal transportation with hoverboards.",
url: "/tech/hoverboard"
},
{
title: "Biff Tannen's Antagonism",
description: "Explore the many timelines where Biff makes life difficult for Marty.",
url: "/characters/biff"
},
{
title: "Flux Capacitor",
description: "The core component that makes time travel possible.",
url: "/tech/flux-capacitor"
},
{
title: "1985 vs. 2015",
description: "Compare the original 1985 to the future version envisioned in Part II.",
url: "/timeline/1985-vs-2015"
},
{
title: "Enchantment Under the Sea Dance",
description: "The pivotal high school dance that almost erased Marty from existence.",
url: "/events/enchantment-dance"
},
{
title: "Back to the Future Merchandise",
description: "Browse collectibles, clothes, and posters from the BTTF universe.",
url: "/shop/merchandise"
}
];
How Search Works
There is no secret to searching in Vanilla JS. We use a filter.
items.filter(item => item.title.includes(q));
Now, we must check for lowercase, check both title and description, deal with white space before and after, and handle signals.
useSearch
For our first simplest version, we create a composable.
export function useSearch() {
const query = ref('');
const results = ref<BTTFItem[]>([]);
function search() {
const q = query.value.trim().toLowerCase();
results.value = q === ''
? []
: items.filter(item =>
item.title.toLowerCase().includes(q) ||
item.description.toLowerCase().includes(q)
);
}
return {
query,
results,
search
};
}
This composable searches through our items
array and filters out the results.
Notice the items
array is declared outside the hook itself, as we only need to declare the static data once.
Usage
We need to import the the composable and use it in our search component. Make sure to use proper separation of concerns and the single responsibility principle.
Keep the functionality in the composable.
<script setup lang="ts">
const { query, results, search } = useSearch();
</script>
<template>
<div class="p-6 max-w-xl mx-auto relative">
<input
type="text"
v-model="query"
@input="search"
placeholder="Search Back to the Future..."
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ul
v-if="query && results.length"
class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg max-h-80 overflow-y-auto"
>
<li
v-for="(item, index) in results"
:key="index"
class="hover:bg-gray-50 border-b border-gray-100 last:border-0"
>
<NuxtLink
:to="item.url"
class="block px-4 py-3 text-blue-600 font-medium"
>
{{ item.title }}
</NuxtLink>
</li>
</ul>
</transition>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<p
v-if="query && !results.length"
class="absolute left-0 right-0 mt-2 z-50 bg-white border border-gray-200 rounded-lg shadow-lg px-4 py-3 text-center text-gray-500 italic"
>
No results found.
</p>
</transition>
</div>
</template>
Vue has some beautiful transition effects that work great with Tailwind.
For basic filtering examples:
Fuzzy Search
I know, I know, you want more! You want to implement a basic fuzzy search too!
Version 1
function fuzzyMatch(source: string, target: string): boolean {
source = source.toLowerCase();
target = target.toLowerCase();
let sIndex = 0;
for (let i = 0; i < target.length; i++) {
if (source[sIndex] === target[i]) {
sIndex++;
}
if (sIndex === source.length) {
return true;
}
}
return false;
}
export function useSearch() {
const query = ref('');
const results = ref<BTTFItem[]>([]);
function search() {
const q = query.value.trim().toLowerCase();
if (q === '') {
results.value = [];
return;
}
results.value = items.filter(item =>
fuzzyMatch(q, item.title) || fuzzyMatch(q, item.description)
);
}
return {
query,
results,
search
};
}
The fuzzyMatch
algorithm checks if all characters match in order or not.
Example 1
fuzzyMatch("doc", "Doc Emmett Brown") // true
- Matches:
d
→ found at index 0,o
→ index 1,c
→ index 2 (all in order ✅)
Example 2
fuzzyMatch("mcf", "Marty McFly") // true
- Matches:
m
(Marty),c
(McFly),f
(McFly)—all in order ✅
Example 3
fuzzyMatch("dcm", "Doc Emmett Brown") // false
d
matches,c
matches—but there’s nom
afterc
in the string ❌
If you want to ignore spaces, you can add:
source = source.replace(/\s+/g, '').toLowerCase();
target = target.replace(/\s+/g, '').toLowerCase();
Scoring
Now, let’s score the results so we can sort the order.
function fuzzyScore(query: string, text: string) {
query = query.replace(/\s+/g, '').toLowerCase();
text = text.replace(/\s+/g, '').toLowerCase();
let score = 0;
let lastIndex = -1;
for (const char of query) {
const index = text.indexOf(char, lastIndex + 1);
if (index === -1) return 0;
score += 1 / (index - lastIndex);
lastIndex = index;
}
return score;
}
export function useSearch() {
const query = ref('');
const results = ref<BTTFItem[]>([]);
function search() {
const q = query.value.trim().toLowerCase();
if (!q) {
results.value = [];
return;
}
results.value = items
.map(item => {
const scoreTitle = fuzzyScore(q, item.title);
const scoreDesc = fuzzyScore(q, item.description);
const totalScore = scoreTitle * 2 + scoreDesc;
return { item, score: totalScore };
})
.filter(entry => entry.score > 0)
.sort((a, b) => b.score - a.score)
.map(entry => entry.item);
}
return {
query,
results,
search
};
}
Instead of returning a boolean
, we return a score
and use .sort()
to sort by that score. This includes the description
AND the title
.
Beautiful!
This is just the beginning of what you can do, but now you have the fundamentals!
The future isn’t written yet.
This content originally appeared on Telerik Blogs and was authored by Jonathan Gamble

Jonathan Gamble | Sciencx (2025-08-22T14:15:00+00:00) Searching Your Site Without a Database. Retrieved from https://www.scien.cx/2025/08/22/searching-your-site-without-a-database/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.