Adding search functionality is a common challenge for static sites. Many developers install large indexing libraries (like ElasticLunr or Fuse.js) or wire up third-party services. But if your blog has fewer than 100 articles, loading a large external framework is completely unnecessary.
This guide shows you how to implement search using **zero frameworks, zero servers, and zero databases**. We will pre-build a tiny JSON index of our posts during development, and query it directly using a few lines of vanilla JavaScript.
Step 1: The Build-Time Indexer
We write a short script (run during site generation or deployment) that scans our posts folder, extracts text from the HTML, and outputs a compact search-index.json file:
# build_index.py
import os
import json
import re
from html.parser import HTMLParser
class SimpleHTMLParser(HTMLParser):
def __init__(self):
super().__init__()
self.text_content = []
def handle_data(self, data):
self.text_content.append(data)
def generate_index(posts_dir="posts", output_file="search-index.json"):
index = []
for file in os.listdir(posts_dir):
if file.endswith(".html"):
path = os.path.join(posts_dir, file)
with open(path, "r", encoding="utf-8") as f:
html = f.read()
# Simple metadata extraction
title_match = re.search(r"<title>(.*?)</title>", html)
title = title_match.group(1).replace(" — Next Harizon", "") if title_match else file
parser = SimpleHTMLParser()
parser.feed(html)
text = " ".join(parser.text_content)
# Remove extra spacing
text = re.sub(r'\s+', ' ', text).strip()
index.append({
"title": title,
"url": f"posts/{file}",
"text": text[:600].lower() # index the first 600 chars for small index size
})
with open(output_file, "w", encoding="utf-8") as f:
json.dump(index, f, separators=(',', ':'))
if __name__ == "__main__":
generate_index()
Step 2: Designing the Search Overlay
To avoid slowing down initial page loads, we will only download the search-index.json file when the user clicks or focuses on the search bar. This is a crucial trick to keep the initial page loading times low.
<input type="text" id="search-input" placeholder="Search articles..." autocomplete="off">
<div id="search-results"></div>
Step 3: Lightweight Querying
The query matching logic splits the query into keywords and searches the static JSON object. It takes under 50 lines of plain JavaScript:
let searchIndex = null;
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
// Download index only when the search input is focused
searchInput.addEventListener('focus', async () => {
if (searchIndex !== null) return;
try {
const res = await fetch('/search-index.json');
searchIndex = await res.json();
} catch (err) {
console.error("Failed to load search index", err);
}
});
searchInput.addEventListener('input', () => {
if (!searchIndex) return;
const query = searchInput.value.toLowerCase().trim();
searchResults.innerHTML = '';
if (!query) return;
const keywords = query.split(/\s+/);
const matches = searchIndex.filter(post => {
return keywords.every(kw => post.title.toLowerCase().includes(kw) || post.text.includes(kw));
});
matches.forEach(match => {
const item = document.createElement('a');
item.href = match.url;
item.className = 'search-result-item';
item.innerHTML = `<strong>${match.title}</strong><p>${match.text.substring(0, 100)}...</p>`;
searchResults.appendChild(item);
});
});
Conclusion
By shifting index calculation to build time and loading the static JSON file on focus, search becomes instant. It utilizes the browser's native array parsing engine which is highly optimized, ensuring compatibility and low CPU usage without external plugins.