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.