Search and islands
One JSON endpoint, one client component, no framework.
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:
| Match | Points |
|---|---|
| Title, at the start | 12 |
| Title, anywhere | 8 |
| Description | 4 |
| Body text | 2 |
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.