zgba Network

Client-Side Search From a Prebuilt JSON Index: Zero Dependencies, Zero Server Calls

I run The DJ Calendar (https://thedjcalendar.com/) — a site with hundreds of artist pages. Every page has a search bar in the top-right corner that searches across every artist on the site. It works instantly. No loading spinner. No debounce delay. No network request at all. Here’s how I built it with a prebuilt JSON index and zero dependencies. The Problem A site with hundreds of artists needs search. The obvious options are: Algolia / Meilisearch — great products, but adds a dependency, a monthly bill, and a server round-trip on every keystroke. Server-side search — hits your database on every keystroke. Fine for low traffic, but each search is a full HTTP request with latency. Lunr / Fuse.js — client-side libraries, but they’re dependencies you don’t control. I wanted search that felt instant because it was instant. No network, no loading, no third-party script. The Approach: Prebuilt JSON Index At deploy time, a PHP script crawls the artist database and writes a flat JSON file. Every artist gets one entry with their name, genre, location, and a URL. The file sits on the server as static JSON — no PHP involved at runtime. [ { “name”: “David Guetta”, “slug”: “david-guetta”, “genres”: [“EDM”, “House”, “Pop”], “locations”: [“Paris”, “Ibiza”, “Las Vegas”], “url”: “https://thedjcalendar.com/artist/david-guetta” }, … ] At page load, the search script fetches this JSON file once and stores it in memory. Every keystroke searches the local array. Scored AND/OR Matching The search function does scored matching across weighted fields. Name scores highest, genre and location score lower. A query like “david house” matches artists whose name OR genre contains “david” AND “house”. function search(query, items) { var terms = query.toLowerCase().split(' '); var results = []; for (var i = 0; i < items.length; i++) { var item = items[i]; var score = 0; var matchCount = 0; for (var t = 0; t < terms.length; t++) { var term = terms[t]; var fieldScore = 0; // Name match — highest weight if (item.name.toLowerCase().includes(term)) { fieldScore = Math.max(fieldScore, 10); } // Genre match — medium weight for (var g = 0; g < item.genres.length; g++) { if (item.genres[g].toLowerCase().includes(term)) { fieldScore = Math.max(fieldScore, 5); } } // Location match — lower weight for (var l = 0; l < item.locations.length; l++) { if (item.locations[l].toLowerCase().includes(term)) { fieldScore = Math.max(fieldScore, 3); } } if (fieldScore > 0) matchCount++; score += fieldScore; } // AND: all terms must match at least one field if (matchCount === terms.length) { results.push({ item: item, score: score }); } } results.sort(function(a, b) { return b.score - a.score; }); return results.slice(0, 20); } The AND requirement prevents irrelevant results. If you type “las vegas pop”, you get artists in Las Vegas whose genre is Pop — not every artist in Las Vegas. ARIA Combobox The search UI is a standard ARIA combobox pattern. The input has role=“combobox”, the results list has role=“listbox”, and each result is role=“option”. Keyboard navigation: down/up arrows cycle through results, Enter selects, Escape closes. Focus is trapped in the combobox while it’s open. The active option is announced via aria-activedescendant. Why This Matters This pattern works for any collection that fits in memory — artists, products, documentation pages, blog posts. The index is generated at build time, so there’s zero runtime cost. The JSON file is cacheable by the browser and CDN. I extracted this into a standalone repo: instant-search-index (https://github.com/bryanhamiltondev/instant-search-index). It includes the full combobox implementation with keyboard navigation, plus a PHP index builder you can adapt to any data source. Lessons Learned JSON size matters. My index is ~150 KB gzipped. For larger datasets, chunk the index or lazy-load it. Scoring needs tuning. Start with simple weights and adjust based on real queries. AND matching is opinionated. Some search UIs work better with OR. Make it configurable. The ARIA pattern is non-negotiable. Search is one of the most-used features. It has to work for keyboard and screen reader users. The full code, demo, and PHP index builder are on GitHub (https://github.com/bryanhamiltondev/instant-search-index). The live version runs on The DJ Calendar (https://thedjcalendar.com/https://thedjcalendar.com/). All my production-extracted repos live at github.com/bryanhamiltondev (https://github.com/bryanhamiltondev). My portfolio is at bryanhamilton.info (https://www.bryanhamilton.info/).

View original article