Search a Stati site in the browser

Stati writes the search index at build time. A few dozen lines of TypeScript search it.

SNP 093 #9B5 Filed stati typescript

search: { enabled: true } in the config writes a JSON index of the site's text with every build. Reading it:

typescript
// src/search.ts
interface SearchDocument {
  url: string;
  anchor: string;
  title: string;
  heading: string;
  content: string;
  breadcrumb: string;
}

const input = document.querySelector<HTMLInputElement>('#search');
const results = document.querySelector<HTMLUListElement>('#results');
const meta = document.querySelector<HTMLMetaElement>('meta[name="stati:search-index"]');
let documents: SearchDocument[] | undefined;

async function search(query: string): Promise<SearchDocument[]> {
  // The index's file name changes with every build; every page names it in this meta tag
  documents ??= (await (await fetch(meta!.content)).json()).documents as SearchDocument[];
  const words = query.toLowerCase();
  return documents.filter((doc) => `${doc.title} ${doc.heading} ${doc.content}`.toLowerCase().includes(words));
}

input?.addEventListener('input', async () => {
  if (!results || !meta) return;
  const query = input.value.trim();
  const hits = query.length < 2 ? [] : await search(query);
  results.replaceChildren(
    ...hits.slice(0, 10).map((doc) => {
      const link = document.createElement('a');
      link.href = doc.anchor ? `${doc.url}#${doc.anchor}` : doc.url;
      link.textContent = doc.breadcrumb;
      const item = document.createElement('li');
      item.append(link);
      return item;
    }),
  );
});
html
<input id="search" type="search" placeholder="Search">
<ul id="results"></ul>
  • A document is either a whole page (level: 1, empty anchor) or the section under one heading, with its text in content. The home page and drafts aren't in it.
  • Build the results with textContent. The index holds your own text, but text can have a < in it too.
  • For typo tolerance and ranking, hand the same documents to MiniSearch or Fuse.js. The filters on this site's lists are MiniSearch over this index.

In the Stati docs: Auto-injected meta tag, Search index structure and Implementing client-side search.