mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 15:28:57 +00:00
feat(assets): search UI redesign with heading, metadata and Bing CSE support (#791)
* feat(assets): redesign search result UI with heading, tags, categories and collections - Add heading/anchor display for anchorified search results (inline in title row) - Add tags, categories, collections display in search result metadata row - Extend SearchResult interface with optional heading, tags, categories, collections - Pass metadata through all search engines (Fuse.js, Algolia, Pagefind) - Add search-specific render hooks to eliminate replaceRE hacks: - render-heading.search.json: clean heading output without class/span - render-codeblock.search.json: code output without line numbers - fixit-encryptor.search.json: skip encrypted content in search index - Add data-pagefind-ignore="all" to fixit-encryptor for Pagefind exclusion - Add data-pagefind-meta for tags, categories, collections in Pagefind - Add data-pagefind-sort for title (alphabetical sorting) - Extend PagefindConfig.sortBy to support "title" option - Redesign search modal SCSS with flex layout for title/heading/date row - Add .suggestion-meta row with icon-styled tags/categories/collections * feat(layouts): add Bing Custom Search Engine support - Implement Bing CSE adapter in search engine (redirect to results page) - Add Bing CSE widget embed in search page template via JavaScript snippet - Update CSE engine warning message to be provider-agnostic - Add Bing icon (fa-microsoft) for Bing CSE results - Add cx config field for Bing and mark as unverified
This commit is contained in:
@@ -65,17 +65,26 @@ export function createAlgoliaEngine(searchConfig: SearchConfig): SearchEngine {
|
||||
const { hits } = await algoliaIndex!.search(query, {
|
||||
offset: 0,
|
||||
length: maxResultLength * 8,
|
||||
attributesToHighlight: ['title'],
|
||||
attributesToHighlight: ['title', 'heading'],
|
||||
attributesToRetrieve: ['*'],
|
||||
attributesToSnippet: [`content:${snippetLength}`],
|
||||
highlightPreTag: `<${highlightTag}>`,
|
||||
highlightPostTag: `</${highlightTag}>`,
|
||||
})
|
||||
const results: Record<string, SearchResult> = {}
|
||||
hits.forEach(({ uri, date, _highlightResult: { title }, _snippetResult: { content } }: any) => {
|
||||
hits.forEach(({ uri, date, heading, tags, categories, collections, _highlightResult, _snippetResult: { content } }: any) => {
|
||||
if (results[uri] && results[uri].context.length > content.value.length)
|
||||
return
|
||||
results[uri] = { uri, title: title.value, date, context: content.value }
|
||||
results[uri] = {
|
||||
uri,
|
||||
title: _highlightResult.title.value,
|
||||
date,
|
||||
context: content.value,
|
||||
heading: _highlightResult.heading?.value || heading || undefined,
|
||||
tags,
|
||||
categories,
|
||||
collections,
|
||||
}
|
||||
})
|
||||
return Object.values(results).slice(0, maxResultLength)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CSEConfig, SearchEngine, SearchResult } from '../types'
|
||||
|
||||
/**
|
||||
* Create a Google Custom Search Engine adapter.
|
||||
* Create a Custom Search Engine adapter (Google or Bing).
|
||||
*
|
||||
* Returns a single result that links to the CSE results page.
|
||||
* @param cseConfig - The CSE configuration, or `undefined` if not configured.
|
||||
* @returns A SearchEngine instance for Google CSE.
|
||||
* @returns A SearchEngine instance for the configured CSE provider.
|
||||
*/
|
||||
export function createCSEEngine(cseConfig: CSEConfig | undefined): SearchEngine {
|
||||
return {
|
||||
@@ -19,12 +19,21 @@ export function createCSEEngine(cseConfig: CSEConfig | undefined): SearchEngine
|
||||
icon: '<i class="fa-brands fa-google" aria-hidden="true"></i>',
|
||||
}]
|
||||
}
|
||||
console.warn('CSE is not properly configured. Please set cse.engine to "google" and provide a cx value in your site config.')
|
||||
if (cseConfig?.engine === 'bing' && cseConfig.cx) {
|
||||
return [{
|
||||
uri: `${cseConfig.resultsPage}?q=${encodeURIComponent(query)}`,
|
||||
title: cseConfig.searchIn || '',
|
||||
date: '',
|
||||
context: cseConfig.gotoResultsPage || '',
|
||||
icon: '<i class="fa-brands fa-microsoft" aria-hidden="true"></i>',
|
||||
}]
|
||||
}
|
||||
console.warn('CSE is not properly configured. Please set cse.engine and provide a cx value in your site config.')
|
||||
return [{
|
||||
uri: '',
|
||||
title: 'CSE is not configured',
|
||||
date: '',
|
||||
context: 'Please set <code>cse.engine</code> and <code>cse.google.cx</code> in your site config.',
|
||||
context: 'Please set <code>cse.engine</code> and the corresponding <code>cx</code> value in your site config.',
|
||||
}]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export function createFuseEngine(searchConfig: SearchConfig): SearchEngine {
|
||||
window._fuseIndex!.search(query).forEach(({ item, matches }: any) => {
|
||||
let title = item.title
|
||||
let content = item.content
|
||||
let heading = item.heading || ''
|
||||
matches.forEach(({ indices, key }: any) => {
|
||||
if (key === 'content') {
|
||||
content = applyHighlightToText(content, indices, highlightTag)
|
||||
@@ -25,12 +26,19 @@ export function createFuseEngine(searchConfig: SearchConfig): SearchEngine {
|
||||
else if (key === 'title') {
|
||||
title = applyHighlightToText(title, indices, highlightTag)
|
||||
}
|
||||
else if (key === 'heading') {
|
||||
heading = applyHighlightToText(heading, indices, highlightTag)
|
||||
}
|
||||
})
|
||||
results[item.uri] = {
|
||||
uri: item.uri,
|
||||
title,
|
||||
date: item.date,
|
||||
context: content,
|
||||
heading: item.heading ? heading : undefined,
|
||||
tags: item.tags,
|
||||
categories: item.categories,
|
||||
collections: item.collections,
|
||||
}
|
||||
})
|
||||
return Object.values(results).slice(0, maxResultLength)
|
||||
@@ -53,7 +61,7 @@ export function createFuseEngine(searchConfig: SearchConfig): SearchEngine {
|
||||
includeScore: false,
|
||||
shouldSort: true,
|
||||
includeMatches: true,
|
||||
keys: ['content', 'title'],
|
||||
keys: ['content', 'title', 'heading'],
|
||||
})
|
||||
return doSearch()
|
||||
}
|
||||
|
||||
@@ -124,12 +124,25 @@ export function createPagefindEngine(searchConfig: SearchConfig, pagefindConfig:
|
||||
(searched.results || []).slice(0, resultLimit).map((entry: any) => entry.data()),
|
||||
)
|
||||
|
||||
return records.map((item: any) => ({
|
||||
uri: item.url || '#',
|
||||
title: item.meta?.title || item.url || '',
|
||||
date: item.meta?.date || '',
|
||||
context: replaceExcerptHighlightTag(item.excerpt || '', highlightTag),
|
||||
}))
|
||||
return records.map((item: any) => {
|
||||
const url = item.url || '#'
|
||||
const hashIndex = url.indexOf('#')
|
||||
let heading: string | undefined
|
||||
if (hashIndex > 0 && item.sub_results?.length) {
|
||||
const subResult = item.sub_results.find((sr: any) => sr.url === url)
|
||||
heading = subResult?.title || undefined
|
||||
}
|
||||
return {
|
||||
uri: url,
|
||||
title: item.meta?.title || item.url || '',
|
||||
date: item.meta?.date || '',
|
||||
context: replaceExcerptHighlightTag(item.excerpt || '', highlightTag),
|
||||
heading,
|
||||
tags: item.meta?.tags ? item.meta.tags.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined,
|
||||
categories: item.meta?.categories ? item.meta.categories.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined,
|
||||
collections: item.meta?.collections ? item.meta.collections.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined,
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,14 @@ export class SearchModule implements SearchService {
|
||||
const title = h`<span class="suggestion-title" dangerouslySetInnerHTML=${{ __html: item.title }}></span>`
|
||||
const icon = item.icon ? h`<span class="suggestion-icon" dangerouslySetInnerHTML=${{ __html: item.icon }}></span>` : ''
|
||||
const date = item.date ? h`<span class="suggestion-date">${item.date}</span>` : ''
|
||||
const heading = item.heading ? h`<span class="suggestion-heading"><span class="suggestion-heading-mark">#</span> <span dangerouslySetInnerHTML=${{ __html: item.heading }}></span></span>` : ''
|
||||
const context = h`<div class="suggestion-context" dangerouslySetInnerHTML=${{ __html: item.context }}></div>`
|
||||
return h`<div class="search-item-wrapper"><div><a href="${item.uri}">${title}</a>${icon}${date}</div>${context}</div>`
|
||||
const cats = item.categories?.length ? h`<span class="suggestion-category"><i class="fa-regular fa-folder" aria-hidden="true"></i> ${item.categories.join(', ')}</span>` : ''
|
||||
const cols = item.collections?.length ? h`<span class="suggestion-collection"><i class="fa-solid fa-layer-group" aria-hidden="true"></i> ${item.collections.join(', ')}</span>` : ''
|
||||
const tags = item.tags?.length ? h`<span class="suggestion-tag"><i class="fa-solid fa-tags" aria-hidden="true"></i> ${item.tags.join(', ')}</span>` : ''
|
||||
const hasMeta = cats || cols || tags
|
||||
const meta = hasMeta ? h`<div class="suggestion-meta">${cats}${cols}${tags}</div>` : ''
|
||||
return h`<div class="search-item-wrapper"><div><a href="${item.uri}">${title}</a>${icon}${heading}${date}</div>${context}${meta}</div>`
|
||||
},
|
||||
noResults({ html: h }: { html: any }) {
|
||||
return h`<div class="search-empty"><i class="fa-solid fa-magnifying-glass search-empty-icon" aria-hidden="true"></i><p>${searchConfig.noResultsFound}: <span class="search-query">"${query}"</span></p></div>`
|
||||
|
||||
@@ -7,6 +7,10 @@ export interface SearchResult {
|
||||
date: string
|
||||
context: string
|
||||
icon?: string
|
||||
heading?: string
|
||||
tags?: string[]
|
||||
categories?: string[]
|
||||
collections?: string[]
|
||||
}
|
||||
|
||||
/** Common interface for all search engine backends. */
|
||||
@@ -45,7 +49,7 @@ export interface PagefindConfig {
|
||||
bundlePath?: string
|
||||
debounceTimeoutMs?: number
|
||||
useBuiltInFilters?: boolean
|
||||
sortBy?: 'date'
|
||||
sortBy?: 'date' | 'title'
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user