A free and simple Algolia and TypeSense Alternative with Para backend
HTML
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/daisyui/5.1.25/daisyui.css">
<button class="btn btn-ghost btn-sm" onclick="site_search_modal.showModal()" aria-label="Open search" title="Search '/' or Ctrl+K">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<path d="M20 20l-3.5-3.5"></path>
</svg>
SEARCH
</button>
<dialog id="site_search_modal" class="modal">
<div class="top-24 absolute xs:w-full sm:w-10/12 md:w-6/12 max-w-3xl modal-box">
<div class="mt-1">
<div class="flex items-center gap-2 w-full input input-bordered">
<svg class="opacity-70 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<path d="M20 20l-3.5-3.5"></path>
</svg>
<input id="site-search-input" type="search" list="options" class="grow" placeholder="Search…"
role="combobox" aria-controls="searchbox" aria-expanded="true" aria-autocomplete="list"/>
</div>
</div>
<ul id="site-search-results" class="space-y-2 mt-3 text-sm"></ul>
<div class="flex pt-4 text-xs">
<span class="grow">
<kbd class="kbd kbd-sm">▲</kbd> <kbd class="kbd kbd-sm">▼</kbd>
<kbd class="kbd kbd-sm">Enter</kbd> to select,
<kbd class="kbd kbd-sm">Esc</kbd> to close
</span>
<strong class="justify-end text-right">
<a href="https://paraio.org">Search powered by Para</a>
</strong>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
Tailwind CSS
/* There's nothing here - CSS is embedded in the HTML. */
JavaScript
const appID = "app:myapp";
const isLocal = false;
const paraEndpoint = isLocal ? "http://localhost:8000" : "https://paraio.com";
const searchToggle = document.getElementById("site_search_modal");
const searchInput = document.getElementById("site-search-input");
const resultsList = document.getElementById("site-search-results");
const noResults = "<li>No Results</li>";
let searchAbortController;
let searchDebounce;
let activeIndex = -1;
const openSearch = () => {
if (searchToggle) {
searchToggle.showModal();
}
};
const renderResults = (items) => {
if (!resultsList) return;
resultsList.innerHTML = items.length === 0 ? noResults : "";
activeIndex = -1;
items.forEach((item) => {
const li = document.createElement("li");
const link = document.createElement("a");
link.href = item.url;
link.textContent = item.name;
link.className = "block rounded-[3px] px-3 py-3";
li.className = "border border-base-300 hover:border-sky-700 rounded-[3px]";
li.appendChild(link);
resultsList.appendChild(li);
});
};
const updateActiveResult = (index) => {
if (!resultsList) return;
const items = Array.from(resultsList.querySelectorAll("li"));
items.forEach((item, i) => {
if (i === index) {
item.classList.add("border-sky-700", "bg-base-200");
} else {
item.classList.remove("border-sky-700", "bg-base-200");
}
});
};
const search = async (query) => {
if (!resultsList) return;
const trimmed = query.trim();
if (!trimmed) {
resultsList.innerHTML = "";
return;
}
if (searchAbortController) {
searchAbortController.abort();
}
searchAbortController = new AbortController();
try {
const response = await fetch(
`${paraEndpoint}/v1/blogposts?limit=6&q=${encodeURIComponent(trimmed || "*")}`,
{
signal: searchAbortController.signal,
headers: {
Authorization: "Anonymous " + appID,
},
...