search: make the navigation form work from every page (AUD-151)
The nav form carried hx-get="/search" hx-target="#search-results", but #search-results exists only on /search. On every other page htmx found no target and silently discarded the response: a search box that looks like it works and does nothing. - SearchForm is now a plain GET with no htmx attributes (navigation and the landing page), so it navigates to /search?q=… from any page and with JavaScript disabled. - SearchFormLive keeps the in-place swap on the search page, where the target does exist. For HX-Request the server now returns the block's *contents* (SearchResultsBody), so the fragment no longer nests a second element with id="search-results" inside the live region. - Empty query shows a prompt instead of an empty list. A zero-match query says "Nothing found" and offers the six areas plus the evidence index. - The result count is role="status" inside the existing aria-live container, so a screen reader reads "4 results for …" rather than a run of titles. Tests (internal/server, internal/site): - the nav form carries no htmx attribute on nine pages, and no built page carries an hx-target whose element is missing (walks all of dist/); - search submitted from /, /edge/ and /platform/ lands on a page with results with no HX-Request header, i.e. the JavaScript-disabled path; - empty query, zero match and the announced count; - a content change followed by a build is visible in dist/search.db. That is the deploy path: deploy/deploy.sh runs `make dist` before it stages a release, and search.Build removes and rewrites the index every build. DoD: search from home, /edge and /platform lands on results; works with JavaScript disabled; zero-match query shows a message; content change followed by a deploy is reflected in search; no htmx request targets a missing element. (cherry picked from commit a56c795768e1e9b10204aae04c3612d7a96775a7)
This commit is contained in:
@@ -135,40 +135,67 @@ templ SearchPage(v SearchView) {
|
||||
<h1>Search { strconv.Itoa(v.Indexed) } pages</h1>
|
||||
<p class="doc-claim">Full-text search over page titles and body text. Served from a local index — no model, no network call.</p>
|
||||
</header>
|
||||
@SearchForm(v.Query, "page-search")
|
||||
@SearchFormLive(v.Query, "page-search")
|
||||
@SearchResults(v)
|
||||
</article>
|
||||
}
|
||||
}
|
||||
|
||||
// SearchResults is swapped into #search-results by HTMX.
|
||||
// SearchResults is the results block as a page element: the live region wrapper
|
||||
// plus its contents, which is what the whole page renders. Without JavaScript the
|
||||
// form reloads the page and this markup arrives in the document.
|
||||
templ SearchResults(v SearchView) {
|
||||
<div id="search-results" aria-live="polite">
|
||||
if v.Query == "" {
|
||||
<p class="section-note">Try “zigbee”, “signage”, “routing”, “backups” or “guardrails”.</p>
|
||||
} else if v.Failed {
|
||||
<p class="search-empty">That query could not be run. Try fewer or simpler words.</p>
|
||||
} else if len(v.Results) == 0 {
|
||||
<p class="search-empty">No page matches <q>{ v.Query }</q>. The <a href="/evidence/">evidence index</a> lists everything by area.</p>
|
||||
} else {
|
||||
<p class="search-count">{ strconv.Itoa(len(v.Results)) } results for <q>{ v.Query }</q></p>
|
||||
<ol class="results">
|
||||
for _, r := range v.Results {
|
||||
<li class="result">
|
||||
<h2><a href={ templ.SafeURL(r.URL) }>{ r.Title }</a></h2>
|
||||
<p class="result-meta">
|
||||
<span class="kind">{ r.Area }</span>
|
||||
<span class="kind">L{ strconv.Itoa(r.Level) }</span>
|
||||
<span class={ ConfidenceClass(r.Confidence) }>{ r.Confidence }</span>
|
||||
</p>
|
||||
<p class="result-snippet">@SafeSnippet(r.Snippet)</p>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
@SearchResultsBody(v)
|
||||
</div>
|
||||
}
|
||||
|
||||
// SearchResultsBody is the results block without the live region. This is what an
|
||||
// htmx request returns: the search page's form swaps it into #search-results with
|
||||
// innerHTML, so returning the wrapper too would nest a second element with the
|
||||
// same id inside the first and make the live region announce twice.
|
||||
//
|
||||
// Everything here arrives inside the polite live region, and the count carries
|
||||
// role="status" on top of that, so the number is read as one utterance instead of
|
||||
// a run of result titles.
|
||||
templ SearchResultsBody(v SearchView) {
|
||||
if v.Query == "" {
|
||||
<p class="search-empty">Type one or two words above and press Enter. Good starting points: “zigbee”, “signage”, “routing”, “backups”, “guardrails”.</p>
|
||||
} else if v.Failed {
|
||||
<p class="search-empty">That query could not be run. Try fewer or simpler words.</p>
|
||||
} else if len(v.Results) == 0 {
|
||||
<p class="search-empty">Nothing found for <q>{ v.Query }</q>. Try a shorter or more general word, or browse an area:</p>
|
||||
@searchAreaLinks(v.Areas)
|
||||
} else {
|
||||
<p class="search-count" role="status">{ strconv.Itoa(len(v.Results)) } results for <q>{ v.Query }</q></p>
|
||||
<ol class="results">
|
||||
for _, r := range v.Results {
|
||||
<li class="result">
|
||||
<h2><a href={ templ.SafeURL(r.URL) }>{ r.Title }</a></h2>
|
||||
<p class="result-meta">
|
||||
<span class="kind">{ r.Area }</span>
|
||||
<span class="kind">L{ strconv.Itoa(r.Level) }</span>
|
||||
<span class={ ConfidenceClass(r.Confidence) }>{ r.Confidence }</span>
|
||||
</p>
|
||||
<p class="result-snippet">@SafeSnippet(r.Snippet)
|
||||
</p>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
}
|
||||
}
|
||||
|
||||
// searchAreaLinks is the way out of an empty result: the six areas plus the
|
||||
// evidence index, so a failed search never leaves the reader at a dead end.
|
||||
templ searchAreaLinks(areas []LinkView) {
|
||||
<ul class="search-areas">
|
||||
for _, a := range areas {
|
||||
<li><a href={ templ.SafeURL(a.Href) }>{ a.Label }</a></li>
|
||||
}
|
||||
<li><a href="/evidence/">Evidence index</a></li>
|
||||
</ul>
|
||||
}
|
||||
|
||||
// StreamPage proves the panel can be re-pointed at a stream. The layout and the
|
||||
// panel markup are the ones every other page uses; only the binding differs.
|
||||
templ StreamPage(v StreamView) {
|
||||
|
||||
Reference in New Issue
Block a user