diff --git a/assets/js/modules/search/engines/algolia.ts b/assets/js/modules/search/engines/algolia.ts index 1f0dc751..a3c3738e 100644 --- a/assets/js/modules/search/engines/algolia.ts +++ b/assets/js/modules/search/engines/algolia.ts @@ -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: ``, }) const results: Record = {} - 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) } diff --git a/assets/js/modules/search/engines/cse.ts b/assets/js/modules/search/engines/cse.ts index 17313bbb..86009afd 100644 --- a/assets/js/modules/search/engines/cse.ts +++ b/assets/js/modules/search/engines/cse.ts @@ -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: '', }] } - 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: '', + }] + } + 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 cse.engine and cse.google.cx in your site config.', + context: 'Please set cse.engine and the corresponding cx value in your site config.', }] }, } diff --git a/assets/js/modules/search/engines/fuse.ts b/assets/js/modules/search/engines/fuse.ts index c8ad8054..803fc8c1 100644 --- a/assets/js/modules/search/engines/fuse.ts +++ b/assets/js/modules/search/engines/fuse.ts @@ -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() } diff --git a/assets/js/modules/search/engines/pagefind.ts b/assets/js/modules/search/engines/pagefind.ts index cec58781..0633f877 100644 --- a/assets/js/modules/search/engines/pagefind.ts +++ b/assets/js/modules/search/engines/pagefind.ts @@ -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, + } + }) }, } } diff --git a/assets/js/modules/search/index.ts b/assets/js/modules/search/index.ts index 3862a233..acb30821 100644 --- a/assets/js/modules/search/index.ts +++ b/assets/js/modules/search/index.ts @@ -112,8 +112,14 @@ export class SearchModule implements SearchService { const title = h`` const icon = item.icon ? h`` : '' const date = item.date ? h`${item.date}` : '' + const heading = item.heading ? h`# ` : '' const context = h`
` - return h`
${title}${icon}${date}
${context}
` + const cats = item.categories?.length ? h` ${item.categories.join(', ')}` : '' + const cols = item.collections?.length ? h` ${item.collections.join(', ')}` : '' + const tags = item.tags?.length ? h` ${item.tags.join(', ')}` : '' + const hasMeta = cats || cols || tags + const meta = hasMeta ? h`
${cats}${cols}${tags}
` : '' + return h`
${title}${icon}${heading}${date}
${context}${meta}
` }, noResults({ html: h }: { html: any }) { return h`

${searchConfig.noResultsFound}: "${query}"

` diff --git a/assets/js/modules/search/types.ts b/assets/js/modules/search/types.ts index 6e9702a9..285e39d7 100644 --- a/assets/js/modules/search/types.ts +++ b/assets/js/modules/search/types.ts @@ -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' } diff --git a/assets/scss/widgets/_search-modal.scss b/assets/scss/widgets/_search-modal.scss index dc3b69ae..5d435319 100644 --- a/assets/scss/widgets/_search-modal.scss +++ b/assets/scss/widgets/_search-modal.scss @@ -187,13 +187,13 @@ dialog.search-dialog { > :first-child { display: flex; - align-items: center; - justify-content: space-between; - gap: 0.25rem; - margin-bottom: 0.5rem; + align-items: baseline; + gap: 0.5rem; + margin-bottom: 0.375rem; > a { - display: inline-block; + flex: 1; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -208,9 +208,29 @@ dialog.search-dialog { font-size: 1.25em; } - .suggestion-date { - font-size: 0.875rem; + .suggestion-heading { flex-shrink: 0; + font-size: 0.8125rem; + color: fi-var(global-font-secondary-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40%; + + .suggestion-heading-mark { + color: fi-var(global-font-tertiary-color); + margin-right: 0.125rem; + } + + em { + font-style: normal; + background-color: fi-var(selection-color); + } + } + + .suggestion-date { + flex-shrink: 0; + font-size: 0.8125rem; color: fi-var(global-font-tertiary-color); } @@ -226,6 +246,19 @@ dialog.search-dialog { @include overflow-wrap(break-word); } + .suggestion-meta { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.75rem; + margin-top: 0.375rem; + font-size: 0.75rem; + color: fi-var(global-font-tertiary-color); + + i { + margin-right: 0.125rem; + } + } + em { font-style: normal; background-color: fi-var(selection-color); diff --git a/hugo.toml b/hugo.toml index b55c8183..944e1aec 100644 --- a/hugo.toml +++ b/hugo.toml @@ -576,7 +576,7 @@ bundlePath = "pagefind/" debounceTimeoutMs = 300 # whether to respect FixIt built-in search visibility rules useBuiltInFilters = true -# optional sort field, current recommended built-in value: "date" +# optional sort field: ["date", "title"] sortBy = "" # sort order for sortBy: ["asc", "desc"] sortOrder = "desc" @@ -588,13 +588,15 @@ engine = "google" # search results page URL (layout: search) resultsPage = "/search/" -# Google: https://programmablesearchengine.google.com/ # Google Custom Search Engine Context +# See: https://programmablesearchengine.google.com/ [params.cse.google] cx = "" -# Bing (Unsupported): https://www.customsearch.ai/ +# Bing Custom Search Engine Context (unverified) +# See: https://www.customsearch.ai/ [params.cse.bing] +cx = "" # Header config [params.header] diff --git a/layouts/_markup/render-codeblock.search.json b/layouts/_markup/render-codeblock.search.json new file mode 100644 index 00000000..b92cc807 --- /dev/null +++ b/layouts/_markup/render-codeblock.search.json @@ -0,0 +1 @@ +
{{ .Inner }}
diff --git a/layouts/_markup/render-heading.search.json b/layouts/_markup/render-heading.search.json new file mode 100644 index 00000000..eb3a38f8 --- /dev/null +++ b/layouts/_markup/render-heading.search.json @@ -0,0 +1 @@ +{{ .Text | safeHTML }} diff --git a/layouts/_partials/base/assets.html b/layouts/_partials/base/assets.html index 8e804bea..719b93a4 100644 --- a/layouts/_partials/base/assets.html +++ b/layouts/_partials/base/assets.html @@ -62,12 +62,16 @@ {{- else if eq $search.type "cse" -}} {{- $config = dict "type" "cse" | dict "search" | merge $config -}} {{- $cse := .Site.Params.cse -}} - {{- $config = dict "resultsPage" $cse.resultsPage "gotoResultsPage" (T "assets.gotoResultsPage") | dict "cse" | merge $config -}} + {{- $resultsPage := $cse.resultsPage -}} + {{- if not (hasPrefix $resultsPage (relURL "")) -}} + {{- $resultsPage = path.Join (relURL "") $resultsPage -}} + {{- end -}} + {{- $config = dict "resultsPage" $resultsPage "gotoResultsPage" (T "assets.gotoResultsPage") | dict "cse" | merge $config -}} {{- if (eq $cse.engine "google") | and $cse.google.cx -}} {{- $config = dict "engine" "google" "cx" $cse.google.cx "searchIn" (T "assets.searchIn" "Google") | dict "cse" | merge $config -}} {{- end -}} - {{- if eq $cse.engine "bing" -}} - {{- /* Unsupported */ -}} + {{- if eq $cse.engine "bing" | and $cse.bing.cx -}} + {{- $config = dict "engine" "bing" "cx" $cse.bing.cx "searchIn" (T "assets.searchIn" "Bing") | dict "cse" | merge $config -}} {{- end -}} {{- end -}} {{- end -}} diff --git a/layouts/_partials/plugin/fixit-encryptor.html b/layouts/_partials/plugin/fixit-encryptor.html index 7662827d..dc57a34d 100644 --- a/layouts/_partials/plugin/fixit-encryptor.html +++ b/layouts/_partials/plugin/fixit-encryptor.html @@ -20,7 +20,7 @@ {{- if .IsPartial -}} {{- $id = dict "Page" .Page | partial "function/id.html" -}} {{- end -}} - +
decryptor loading