Render search suggestions on the server

The typeahead requests /_search via htmx (debounced, newer requests
replace in-flight ones) and gets rendered rows with the match
highlighting done in Go. Rows are links, so picking in the header is
plain navigation; the editor's link dialog reads data-name instead. The
editor's [[ autocomplete keeps the JSON response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:00:08 +02:00
co-authored by Claude Opus 5.5
parent 4365fa2567
commit a55a1c8ed4
6 changed files with 266 additions and 257 deletions
+68
View File
@@ -270,3 +270,71 @@ test("mobile: FOLDERS opens the tree in an overlay", async ({ page }) => {
await overlay.getByRole("link", { name: "Projects" }).click();
await expect(overlay).toHaveCount(0);
});
async function seedSearch(page) {
for (const n of ["Zebra Crossing", "Zebra Finch", "Zeta1", "Zeta2", "Zeta3", "Zeta4", "Zeta5", "Zeta6"]) {
mkpage(`Suggest/${n}`, n);
}
expect((await page.request.post("/_reindex")).status()).toBe(204);
}
const suggest = (page) => page.locator("header .suggest-dropdown");
test("header typeahead: highlight, keyboard pick", async ({ page }) => {
await seedSearch(page);
await page.goto("/");
const input = page.locator("header .search-input");
await input.fill("finch");
await expect(suggest(page)).toBeVisible();
await expect(suggest(page).locator(".suggest-row").first().locator("strong")).toHaveText("Finch");
await input.press("ArrowDown");
await expect(suggest(page).locator(".suggest-row.is-active")).toContainText("Zebra Finch");
await input.press("Enter");
await expect(page).toHaveURL(/\/Suggest\/Zebra%20Finch\/$/);
});
test("header typeahead: click, footer, escape, short query", async ({ page }) => {
await seedSearch(page);
await page.goto("/");
const input = page.locator("header .search-input");
await input.fill("z");
await page.waitForTimeout(300);
await expect(suggest(page)).toBeHidden();
await input.fill("zeta");
const footer = suggest(page).locator(".suggest-footer");
const { total } = await (await page.request.get("/_search?q=zeta")).json();
expect(total).toBeGreaterThan(5);
await expect(footer).toHaveText(`Show all ${total} matches`);
await expect(suggest(page).locator(".suggest-row:not(.suggest-footer)")).toHaveCount(5);
await input.press("Escape");
await expect(suggest(page)).toBeHidden();
await input.fill("zebra c");
await suggest(page).locator(".suggest-name", { hasText: "Zebra Crossing" }).click();
await expect(page).toHaveURL(/\/Suggest\/Zebra%20Crossing\/$/);
await page.goto("/");
await page.locator("header .search-input").fill("zeta");
await suggest(page).locator(".suggest-footer").click();
await expect(page).toHaveURL(/\/\?q=zeta$/);
});
test("editor link dialog fills the query from a suggestion", async ({ page }) => {
await seedSearch(page);
await page.goto("/Suggest/Zeta1/?edit");
await page.locator(".cm-content").click();
await page.keyboard.press("Alt+Shift+P");
const query = modal(page).getByPlaceholder("Search query");
await query.fill("crossing");
await modal(page).locator(".suggest-row", { hasText: "Zebra Crossing" }).click();
await expect(query).toHaveValue("Zebra Crossing");
await expect(modal(page).getByPlaceholder("Display text (optional)")).toBeFocused();
await modal(page).getByRole("button", { name: "INSERT" }).click();
await expect(page.locator(".cm-content")).toContainText("[[Zebra Crossing]]");
});
test("editor [[ autocomplete still uses the JSON endpoint", async ({ page }) => {
await seedSearch(page);
await page.goto("/Suggest/Zeta2/?edit");
await page.locator(".cm-content").click();
await page.keyboard.press("Control+End");
await page.keyboard.type("\n[[finc");
await expect(page.locator(".cm-tooltip-autocomplete")).toContainText("Zebra Finch");
});