feat: opt-in Journal search page and JournalSearchBox
- journal.routes.search (off by default) registers journal-search, the results page for ?q= in the API's relevance order (no sort is sent), server-rendered with a Polish-aware result count. - JournalSearchBox: an icon button and a WAI-ARIA combobox with debounced ?fields=suggest suggestions, stale answers dropped, query words marked ignoring diacritics; a plain GET form without JS. - The module keeps the search path out of the route cache and the sitemap and marks it noindex, follow. - Client: suggestPosts and the fields option; useJournalLinks.searchLink.
This commit is contained in:
@@ -43,3 +43,26 @@
|
||||
200 /blog?page=2 contains=<link rel="canonical" href="https://acme.test/blog?page=2">
|
||||
200 /blog/hello-acme contains=og:title
|
||||
200 /blog/hello-acme contains=Acme says hello and introduces its blog.
|
||||
|
||||
# Search (the playground sets journal.routes.search: '/search'). 11 posts
|
||||
# match "acme": 2 pages of 9. Not route-cached (the playground caches /**),
|
||||
# noindex, follow, and left out of the sitemap.
|
||||
200 /search?q=acme contains=Hello Acme
|
||||
200 /search?q=acme contains=11 results
|
||||
200 /search?q=acme contains=noindex, follow
|
||||
200 /search?q=acme&page=2 contains=Acme Note 12
|
||||
404 /search?q=acme&page=3
|
||||
404 /search?q=acme&page=abc
|
||||
200 /search?q=unsafe contains=Unsafe Sample notcontains=Hello Acme
|
||||
200 /search?q=unsafe contains=1 result
|
||||
200 /search?q=zzzzzz contains=Nothing found for “zzzzzz”.
|
||||
200 /search?q=ac contains=Type at least 3 characters.
|
||||
200 /search contains=Type what you are looking for.
|
||||
# The term is text everywhere it is rendered (attributes may keep a bare <).
|
||||
200 /search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E contains=Search results: <script>alert(1) notcontains=>Search results: <script>
|
||||
200 /pl/search?q=acme contains=11 wyników
|
||||
200 /pl/search?q=unsafe contains=1 wynik
|
||||
200 /sitemap.xml notcontains=/search
|
||||
# The header box renders its no-JS form to the search route on every page.
|
||||
200 /blog contains=action="/search"
|
||||
200 /pl/blog contains=action="/pl/search"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// A small node:http stand-in for the anonymous Journal API
|
||||
// (/_journal/api/v1), answering from JSON fixtures. It follows the Go
|
||||
// handlers' query rules (page, per_page clamp, sort allow-list, category with
|
||||
// nested children, tag, search, include_empty) and the author routes, and it
|
||||
// nested children, tag, search, fields=suggest, include_empty) and the author routes, and it
|
||||
// records every request so a smoke run can assert header forwarding.
|
||||
//
|
||||
// Plain .mjs with JSDoc types: Node does not strip TypeScript under
|
||||
@@ -106,6 +106,19 @@ function paginate(rows, page, perPage) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The ?fields=suggest row of the Go serializer. @param {any} post */
|
||||
function suggestion(post) {
|
||||
const text = String(post.excerpt || post.content_html || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
return {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
excerpt: text.length > 160 ? `${text.slice(0, 160).replace(/\s+\S*$/, '')}…` : text,
|
||||
featured_image_url: post.featured_image_url ?? null,
|
||||
published_at: post.published_at,
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {any} post */
|
||||
function sibling(post) {
|
||||
return post ? { id: post.id, title: post.title, slug: post.slug } : null
|
||||
@@ -172,7 +185,9 @@ export function startFixtureApi({ dir, port = 0 }) {
|
||||
rows = rows.filter((p) => p.title.toLowerCase().includes(s))
|
||||
}
|
||||
rows = sortRows(rows, parseSort(q.get('sort')))
|
||||
return paginate(rows, parsePage(q.get('page')), parsePerPage(q.get('per_page')))
|
||||
const out = paginate(rows, parsePage(q.get('page')), parsePerPage(q.get('per_page')))
|
||||
if (q.get('fields') !== 'suggest') return out
|
||||
return { ...out, data: out.data.map(suggestion) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user