feat(search): add server-side search with navbar ui across themes#101
Merged
Conversation
Pure tokenize/index/score service (exact + prefix tiers, weighted title > tags > description > body, multi-token AND, date tie-break). GET /search?q= returns top 8 results as JSON with Cache-Control: no-store; drafts only match for admins. The built index is cached under two audience-separated keys and pre-warmed by the webhook; responses are never cached to prevent draft leakage. Registered before the pages catch-all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared _search.html partial (button and input modes via search_mode set before include) and shared search.js (vanilla IIFE: 200ms debounce, AbortController, textContent-only rendering, Cmd/Ctrl+K, Escape, click-outside, arrow-key navigation), both reaching every theme through the default-theme fallback. Button mode in default and terminal, inline input mode in blue-tech, styled per theme. Removes terminal's .nav-container overflow:hidden which clipped the dropdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a server-side post search endpoint to SquishMark and wires a shared navbar search UI into all bundled themes (default, blue-tech, terminal), with tests and theme-creator documentation to support overrides/fallbacks.
Changes:
- Introduces
GET /search?q=backed by a cached, audience-separated (published vs drafts) in-memory search index and a deterministic scorer. - Adds a shared
_search.htmlnavbar partial plussearch.jsbehavior (debounced fetching, keyboard navigation, dropdown UI) and theme-specific CSS for button/input modes. - Adds unit + integration tests validating scoring, JSON shape, draft gating, and cache separation; updates theme-creator docs accordingly.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| themes/terminal/static/css/style.css | Removes navbar overflow clipping and adds terminal-theme dropdown search styling. |
| themes/terminal/base.html | Includes shared search partial (button mode) and loads search.js via static fallback path. |
| themes/default/static/style.css | Adds default-theme search component styles (dropdown, results list, states). |
| themes/default/static/search.js | Implements shared client-side behavior for navbar search (debounce, fetch, keyboard nav, safe DOM rendering). |
| themes/default/base.html | Adds search partial in navbar and loads search.js. |
| themes/default/_search.html | Adds shared search partial supporting button vs input mode markup. |
| themes/blue-tech/static/style.css | Adds blue-tech styling for inline (input mode) navbar search and dropdown. |
| themes/blue-tech/base.html | Includes shared search partial (input mode) and loads search.js. |
| tests/test_search.py | Unit tests for tokenization, scoring/weighting, AND semantics, tie-breaks, and result shape. |
| tests/test_routes_search.py | Integration tests for /search JSON behavior, draft gating, and cache separation across request order. |
| tests/conftest.py | Registers the new integration test module for environment pinning/reset behavior. |
| src/squishmark/services/search.py | Adds search indexing + scoring logic and cached index builder (published vs drafts). |
| src/squishmark/routers/webhooks.py | Warms search indexes after webhook-driven cache refresh. |
| src/squishmark/routers/search.py | Adds /search route with admin-aware draft inclusion and Cache-Control: no-store. |
| src/squishmark/main.py | Registers the new search router before the pages catch-all. |
| .claude/skills/theme-creator/references/template-variables.md | Documents search component contract (mode, hooks, override paths, script include). |
| .claude/skills/theme-creator/references/static-files.md | Documents search.js static fallback behavior and override mechanism. |
Addresses Copilot review: the JSON response now echoes the stripped query it actually executed, and result options initialize aria-selected=false with the active option toggled true for screen-reader feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11
What
Keyword search for posts, accessible from the navbar on every page in all three bundled themes.
Backend
services/search.py— pure, unit-tested scorer: tokenize (lowercase[a-z0-9]+), exact + prefix match tiers, presence-based weights (title 12/8, tags 10/6, description 5/3, body 2/1), multi-token AND, score → date-desc tie-break.GET /search?q=— top 8 results as JSON{query, results: [{title, url, date, tags, excerpt, draft}]}. Queries under 2 chars return empty 200 (client fires per debounce tick). Registered before the pages catch-all.search:index:published/search:index:all, admin key only read behindis_admin);/searchresponses are never cached and carryCache-Control: no-store. The webhook cache-warm pre-builds both variants.Frontend
_search.htmlpartial +search.js(vanilla IIFE, no dependencies) reaching all themes via the default-theme template/static fallbacks; per-theme or per-site overridable.Cmd/Ctrl+K, Escape, click-outside, arrow-key + Enter navigation with combobox/listbox ARIA, draft badge.createElement/textContent(frontmatter is author-supplied; autoescape doesn't cover JSON→DOM). Out-of-order responses guarded by AbortController + current-input check..nav-containeroverflow: hidden, which clipped the dropdown (the sticky-nav risk called out in planning, confirmed and fixed during verification).Docs
search_mode+ include, class hooks, override paths) and thesearch.jsstatic-fallback note.Tests
28 new: 20 unit (
tests/test_search.py— weighting, prefix vs substring, AND semantics, tie-breaks, limits, draft passthrough, result shape) + 8 integration (tests/test_routes_search.py— JSON shape/route reachability, draft hidden anon vs flagged admin, short/no-match queries, and cache-separation in both request orders). Full suite 323 passed;run-checks.py4/4.Verified (dev server + Playwright, user-inspected in browser)
Cmd+K, light + dark dropdown, click-outside; fixed a.nav-links astyle collision on result links.DEV_SKIP_AUTHsees flagged result).Follow-up
Fuzzy matching (typo tolerance) deliberately out of scope — issue to be filed after merge (stdlib
difflibtier,rapidfuzzupgrade path).🤖 Generated with Claude Code