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:
Jakub Zych
2026-10-30 18:14:05 +01:00
parent e0cc87118c
commit 102da43f76
22 changed files with 1170 additions and 14 deletions

View File

@@ -183,3 +183,38 @@ test('getAuthor and listAuthorPosts encode the author and map 404 to null', asyn
assert.equal(await client.getAuthor('nobody'), null)
assert.equal(await client.listAuthorPosts('nobody'), null)
})
test('buildPostListQuery passes fields through only when set', () => {
assert.deepEqual(buildPostListQuery({ search: 'kurki', fields: 'suggest', perPage: 5 }), {
per_page: 5,
search: 'kurki',
fields: 'suggest',
})
assert.equal('fields' in buildPostListQuery({ search: 'kurki' }), false)
})
test('suggestPosts sends search, fields=suggest and per_page, no sort, with the signal', async () => {
const page = { data: [{ id: 1, title: 'Kurki', slug: 'kurki', excerpt: 'x', featured_image_url: null, published_at: null }], meta: { current_page: 1, last_page: 1, per_page: 5, total: 1 } }
const inits: unknown[] = []
const calls: Call[] = []
const fetcher = async (path: string, query?: Record<string, string | number>, init?: { signal?: AbortSignal }) => {
calls.push({ path, query })
inits.push(init)
return page
}
const signal = new AbortController().signal
const client = createJournalClient(fetcher)
assert.deepEqual(await client.suggestPosts(' kurki ', { signal }), page)
assert.deepEqual(calls[0], { path: '/posts', query: { per_page: 5, search: 'kurki', fields: 'suggest' } })
assert.equal((inits[0] as { signal: AbortSignal }).signal, signal)
await client.suggestPosts('kurki', { perPage: 99 })
assert.equal(calls[1]!.query!.per_page, JOURNAL_MAX_PER_PAGE)
})
test('suggestPosts sends nothing for a term under 3 characters', async () => {
const { fetcher, calls } = fakeFetcher({})
const res = await createJournalClient(fetcher).suggestPosts('ąę')
assert.equal(calls.length, 0)
assert.deepEqual(res.data, [])
assert.equal(res.meta.total, 0)
})