Search and islands

One JSON endpoint, one client component, no framework.

Updated 2 min read

The site ships one island of about two kilobytes: the search box. Everything else is server-rendered HTML, and a page that does not render the search box ships no JavaScript at all.

The search endpoint

proc get(ctx: Context): Response {
  const rawQuery = ctx.queryParam("q");
  const limit = ctx.request.query.getInt("limit", SearchIndex.defaultLimit);
  const hits = SearchIndex.query(rawQuery, limit);

  var body = new JsonBuilder();
  body.beginArray();
  for hit in hits {
    body.beginObject();
    body.field("title", hit.title);
    body.field("path", hit.path);
    body.field("excerpt", hit.excerpt);
    body.field("score", hit.score);
    body.endObject();
  }
  body.endArray();

  var response = jsonResponse(body.done());
  response.setHeader("Cache-Control", "public, max-age=60");
  response.setHeader("X-Result-Count", hits.size:string);
  return response;
}

Scoring

The query is trimmed, truncated to 128 bytes and lowercased once. Each document contributes at most one score:

MatchPoints
Title, at the start12
Title, anywhere8
Description4
Body text2

Because the folded title, description and body are precomputed, a query is a byte scan over strings that already exist — no allocation per document, no index rebuild, no second process.

The excerpt is a window around the first body match, expanded outward to word boundaries and bracketed with ellipses.

The response is a plain JSON array, so a client reads payload.length rather than an envelope field. The result count is repeated in an X-Result-Count header for anything that only reads headers.

The client side

The search island debounces at 160 ms, tracks a sequence number so a slow response cannot overwrite a newer one, and builds results with document.createElement and textContent:

const title = document.createElement("span");
title.className = "search-hit-title";
title.textContent = hit.title ?? "";

No innerHTML anywhere. Even if the JSON contained markup, it would render as text.

The link target is checked before it is used:

link.href = typeof hit.path === "string" && hit.path.startsWith("/") ? hit.path : "/";

Why there is no theme toggle

The site renders one palette and always has. A toggle would mean either an inline script to apply the stored preference before first paint — which costs 'unsafe-inline' in the content security policy — or a cookie read on the server plus a client island to write it. Neither is worth a second palette to maintain, so the stylesheet defines one set of custom properties on :root and color-scheme: dark tells the browser to match its own form controls and scrollbars to it.