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:
@@ -9,6 +9,7 @@ import type {
|
||||
JournalPage,
|
||||
JournalPost,
|
||||
JournalPostDetail,
|
||||
JournalPostSuggestion,
|
||||
JournalTag,
|
||||
} from './types.ts'
|
||||
|
||||
@@ -18,8 +19,18 @@ export const JOURNAL_MAX_PER_PAGE = 30
|
||||
/** The Journal API's per_page default. */
|
||||
export const JOURNAL_DEFAULT_PER_PAGE = 9
|
||||
|
||||
/** Issues one GET for `path` (e.g. "/posts") with `query`, resolving the parsed JSON. */
|
||||
export type JournalFetcher = (path: string, query?: Record<string, string | number>) => Promise<unknown>
|
||||
/** Search terms shorter than this are ignored by the API, so the layer never sends them. */
|
||||
export const JOURNAL_SEARCH_MIN_LENGTH = 3
|
||||
|
||||
/**
|
||||
* Issues one GET for `path` (e.g. "/posts") with `query`, resolving the parsed
|
||||
* JSON. `init.signal` aborts the request.
|
||||
*/
|
||||
export type JournalFetcher = (
|
||||
path: string,
|
||||
query?: Record<string, string | number>,
|
||||
init?: { signal?: AbortSignal },
|
||||
) => Promise<unknown>
|
||||
|
||||
export interface PostListOptions {
|
||||
page?: number
|
||||
@@ -30,6 +41,13 @@ export interface PostListOptions {
|
||||
category?: string
|
||||
tag?: string
|
||||
search?: string
|
||||
/** "suggest" asks for JournalPostSuggestion rows instead of full posts. */
|
||||
fields?: 'suggest'
|
||||
}
|
||||
|
||||
export interface SuggestOptions {
|
||||
perPage?: number
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface AuthorPostListOptions {
|
||||
@@ -56,6 +74,7 @@ export function buildPostListQuery(opts: PostListOptions = {}): Record<string, s
|
||||
if (opts.category) q.category = opts.category
|
||||
if (opts.tag) q.tag = opts.tag
|
||||
if (opts.search) q.search = opts.search
|
||||
if (opts.fields) q.fields = opts.fields
|
||||
return q
|
||||
}
|
||||
|
||||
@@ -90,6 +109,11 @@ export function isNotFound(err: unknown): boolean {
|
||||
export interface JournalClient {
|
||||
getPost(slug: string): Promise<JournalPostDetail | null>
|
||||
listPosts(opts?: PostListOptions): Promise<JournalPage<JournalPost>>
|
||||
/**
|
||||
* Suggestion rows for `term` in relevance order (no sort is sent). A term
|
||||
* shorter than JOURNAL_SEARCH_MIN_LENGTH sends nothing and gives an empty page.
|
||||
*/
|
||||
suggestPosts(term: string, opts?: SuggestOptions): Promise<JournalPage<JournalPostSuggestion>>
|
||||
listCategories(): Promise<JournalCategory[]>
|
||||
findCategory(slug: string): Promise<JournalCategory | null>
|
||||
listTags(): Promise<JournalTag[]>
|
||||
@@ -123,6 +147,17 @@ export function createJournalClient(fetcher: JournalFetcher): JournalClient {
|
||||
return (await fetcher('/posts', buildPostListQuery(opts))) as JournalPage<JournalPost>
|
||||
},
|
||||
|
||||
async suggestPosts(term, opts = {}) {
|
||||
const search = term.trim()
|
||||
const perPage = clampPerPage(opts.perPage ?? 5)
|
||||
if ([...search].length < JOURNAL_SEARCH_MIN_LENGTH) {
|
||||
return { data: [], meta: { current_page: 1, last_page: 1, per_page: perPage, total: 0 } }
|
||||
}
|
||||
const query = buildPostListQuery({ search, fields: 'suggest', perPage })
|
||||
const init = opts.signal ? { signal: opts.signal } : undefined
|
||||
return (await fetcher('/posts', query, init)) as JournalPage<JournalPostSuggestion>
|
||||
},
|
||||
|
||||
listCategories,
|
||||
|
||||
async findCategory(slug) {
|
||||
|
||||
Reference in New Issue
Block a user