Skip to content

Commit c1f7f89

Browse files
jdevalkclaude
andauthored
feat(search): migrate to Pagefind Component UI (#71)
Replace the hand-rolled <dialog> + Default UI search with Pagefind's Component UI for both the global ⌘K modal and the /search/ page, plus a mobile search button in the header. - Lazy-load the component bundle on first open to keep ~46 kB off visitors who never search; CSP-safe (external module, no inline JS). - Preserve full-fidelity Plausible analytics via the component's typed event API (search/results), reading the exact result count instead of scraping the rendered DOM. - Keep our own global ⌘K and "/" shortcuts; the component owns the modal, focus, ARIA combobox and keyboard navigation. - Theme the widget to our design tokens in light and dark: map --pf-* on html[lang] to outrank the component's :root defaults, and force color-scheme: inherit past its host reset so light-dark() flips. - Style matched terms with an accent pill and set excerpt length to 30 with multi-line wrapping for more context. - Point the WebMCP open_search tool at the new modal driver. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b38e3a6 commit c1f7f89

11 files changed

Lines changed: 387 additions & 432 deletions

File tree

public/search-init.js

Lines changed: 0 additions & 102 deletions
This file was deleted.

public/search-overlay.js

Lines changed: 0 additions & 200 deletions
This file was deleted.

public/search-page.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Initialiser for the standalone /search/ page. The Pagefind Component UI
2+
// custom elements (loaded eagerly here) drive the search; this script only
3+
// wires Plausible analytics and the ?q= deep-link. No inline scripts, so the
4+
// strict CSP holds. On the dev server the /pagefind/ bundle doesn't exist, so
5+
// the components never upgrade — we detect that and show a build hint.
6+
(function () {
7+
var INSTANCE = "page";
8+
9+
function showBuildHint() {
10+
var mount = document.getElementById("search");
11+
if (!mount) return;
12+
var p = document.createElement("p");
13+
p.className = "text-sm text-ink-600";
14+
p.appendChild(document.createTextNode("Search index is built during "));
15+
var code = document.createElement("code");
16+
code.textContent = "npm run build";
17+
p.appendChild(code);
18+
p.appendChild(
19+
document.createTextNode(
20+
". It is unavailable on the dev server — try the deployed site.",
21+
),
22+
);
23+
mount.replaceChildren(p);
24+
}
25+
26+
// Report searches to Plausible: term, exact result count, and whether it
27+
// matched. The Component UI exposes a typed event API on the search instance,
28+
// so we read the count straight off the `results` event. Debounced so we log
29+
// settled queries. Production only — window.plausible is undefined in dev.
30+
function wireAnalytics(inst) {
31+
var term = "";
32+
var timer;
33+
inst.on("search", function (t) {
34+
term = (t || "").trim().toLowerCase();
35+
});
36+
inst.on("results", function (r) {
37+
var count =
38+
r && typeof r.unfilteredResultCount === "number"
39+
? r.unfilteredResultCount
40+
: null;
41+
clearTimeout(timer);
42+
timer = setTimeout(function () {
43+
if (typeof window.plausible !== "function") return;
44+
if (term.length < 2 || count === null) return;
45+
window.plausible("Search", {
46+
props: {
47+
term: term,
48+
results: count,
49+
found: count > 0,
50+
surface: "page",
51+
},
52+
});
53+
}, 800);
54+
});
55+
}
56+
57+
function prefillFromQuery() {
58+
var q = new URL(window.location.href).searchParams.get("q");
59+
if (!q) return;
60+
var input = document.querySelector('pagefind-input[instance="page"] input');
61+
if (!input) input = document.querySelector("#search input");
62+
if (input) {
63+
input.value = q;
64+
input.dispatchEvent(new Event("input", { bubbles: true }));
65+
}
66+
}
67+
68+
function init() {
69+
if (!window.PagefindComponents) return showBuildHint();
70+
var inst =
71+
window.PagefindComponents.getInstanceManager().getInstance(INSTANCE);
72+
wireAnalytics(inst);
73+
prefillFromQuery();
74+
}
75+
76+
// Race the module's custom-element registration against a timeout: if the
77+
// bundle 404s (dev server) the elements never define and we show the hint.
78+
Promise.race([
79+
customElements.whenDefined("pagefind-input"),
80+
new Promise(function (resolve, reject) {
81+
setTimeout(function () {
82+
reject(new Error("timeout"));
83+
}, 4000);
84+
}),
85+
]).then(init, showBuildHint);
86+
})();

0 commit comments

Comments
 (0)