mirror of
https://github.com/tiennm99/HugoBlox-kit.git
synced 2026-10-12 20:15:25 +00:00
Add built-in search engine
Complement existing integration of Algolia search engine with alternative option of built-in search engine based on Fuse. Setting `engine = 1` in `config.toml` will enable the built-in solution. Close #635
This commit is contained in:
1 parent
3e9f7dab03
commit
a11d8843bb
8 files changed
+254
-16
No files matched your search
@@ -47,6 +47,14 @@
|
||||
version = "3.2.5"
|
||||
sri = "sha256-X5PoE3KU5l+JcX+w09p/wHl9AzK333C4hJ2I9S5mD4M="
|
||||
url = "https://cdnjs.cloudflare.com/ajax/libs/fancybox/%s/jquery.fancybox.min.js"
|
||||
[js.fuse]
|
||||
version = "3.2.1"
|
||||
sri = "sha256-VzgmKYmhsGNNN4Ph1kMW+BjoYJM2jV5i4IlFoeZA9XI="
|
||||
url = "https://cdnjs.cloudflare.com/ajax/libs/fuse.js/%s/fuse.min.js"
|
||||
[js.mark]
|
||||
version = "8.11.1"
|
||||
sri = "sha256-4HLtjeVgH0eIB3aZ9mLYF6E8oU5chNdjU6p6rrXpl9U="
|
||||
url = "https://cdnjs.cloudflare.com/ajax/libs/mark.js/%s/jquery.mark.min.js"
|
||||
[js.instantsearch]
|
||||
version = "2.9.0"
|
||||
sri = "sha256-cJXigylnJlxvAdfFNHeS+IiMcKWS3Rf/cy3bl9bb0ng="
|
||||
|
||||
@@ -306,8 +306,9 @@ enableGitInfo = false
|
||||
[params.search]
|
||||
# Search provider:
|
||||
# 0: No search engine
|
||||
# 1: Algolia (https://www.algolia.com)
|
||||
engine = 0
|
||||
# 1: Built-in (Fuse)
|
||||
# 2: Algolia (https://www.algolia.com)
|
||||
engine = 1
|
||||
|
||||
# Configuration of Algolia search engine.
|
||||
# Paste the values from your Algolia dashboard.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
+++
|
||||
# Search widget.
|
||||
widget = "search"
|
||||
active = false
|
||||
active = true
|
||||
date = 2018-07-23T00:00:00
|
||||
|
||||
title = "Search"
|
||||
|
||||
@@ -247,9 +247,32 @@ small,
|
||||
**************************************************/
|
||||
|
||||
#search-box {
|
||||
position: relative; /* Required for search icon positioning. */
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#search-box::before {
|
||||
content: "\f002";
|
||||
font-family: FontAwesome;
|
||||
font-size: 1rem;
|
||||
opacity: 0.25;
|
||||
line-height: 1rem;
|
||||
position: absolute;
|
||||
left: 0.7rem;
|
||||
top: 0.6rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#search-query {
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 1rem;
|
||||
padding: 1rem 1rem 1rem 2rem; /* Wider left padding for search icon to fit in. */
|
||||
width: 250px;
|
||||
line-height: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.search-hit em {
|
||||
font-style: normal;
|
||||
background-color: #FFE0B2;
|
||||
@@ -1620,6 +1643,10 @@ body.dark {
|
||||
background-color: rgb(68, 71, 90);
|
||||
}
|
||||
|
||||
.dark #search-query {
|
||||
background-color: rgb(68, 71, 90);
|
||||
}
|
||||
|
||||
.dark .label-default {
|
||||
color: rgba(255, 255, 255, .68);
|
||||
background: rgba(255, 255, 255, .2);
|
||||
|
||||
@@ -63,20 +63,37 @@
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
{{ end }}
|
||||
|
||||
{{/* Algolia search engine. */}}
|
||||
{{ if ne .Site.Params.search.engine 0 }}
|
||||
{{/* Configure search engine. */}}
|
||||
<script>
|
||||
const search_index_filename = {{ "/search.json" | relURL }};
|
||||
const i18n = {
|
||||
'placeholder': {{ i18n "search_placeholder" }},
|
||||
'no_results': {{ i18n "search_no_results" }}
|
||||
};
|
||||
const content_type = {
|
||||
'post': {{ i18n "posts" }},
|
||||
'project': {{ i18n "projects" }},
|
||||
'publication' : {{ i18n "publications" }},
|
||||
'talk' : {{ i18n "talks" }}
|
||||
};
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
{{/* Fuse search engine. */}}
|
||||
{{ if eq .Site.Params.search.engine 1 }}
|
||||
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.fuse.url $js.fuse.version) $js.fuse.sri | safeHTML }}
|
||||
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.mark.url $js.mark.version) $js.mark.sri | safeHTML }}
|
||||
<script src="{{ "/js/search.js" | relURL }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{/* Algolia search engine. */}}
|
||||
{{ if eq .Site.Params.search.engine 2 }}
|
||||
{{ if ($scr.Get "use_cdn") }}
|
||||
{{ printf "<script src=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\"></script>" (printf $js.instantsearch.url $js.instantsearch.version) $js.instantsearch.sri | safeHTML }}
|
||||
{{ end }}
|
||||
<script>
|
||||
if ( $('#search-box').length ) {
|
||||
const content_type = {
|
||||
'post': {{ i18n "posts" }},
|
||||
'project': {{ i18n "projects" }},
|
||||
'publication' : {{ i18n "publications" }},
|
||||
'talk' : {{ i18n "talks" }}
|
||||
};
|
||||
|
||||
function getTemplate(templateName) {
|
||||
return document.querySelector(`#${templateName}-template`).innerHTML;
|
||||
}
|
||||
@@ -108,7 +125,7 @@
|
||||
container: '#search-box',
|
||||
autofocus: false,
|
||||
poweredBy: {{ .Site.Params.search.algolia.show_logo | default false }},
|
||||
placeholder: {{ i18n "search_placeholder" }}
|
||||
placeholder: i18n.placeholder
|
||||
})
|
||||
);
|
||||
|
||||
@@ -118,8 +135,8 @@
|
||||
container: '#search-hits',
|
||||
escapeHits: true,
|
||||
templates: {
|
||||
empty: '<div class="search-no-results">' + {{ i18n "search_no_results" }} + '</div>',
|
||||
item: getTemplate('search-hit')
|
||||
empty: '<div class="search-no-results">' + i18n.no_results + '</div>',
|
||||
item: getTemplate('search-hit-algolia')
|
||||
},
|
||||
cssClasses: {
|
||||
showmoreButton: 'btn btn-primary btn-outline'
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.leaflet.url $css.leaflet.version) $css.leaflet.sri | safeHTML }}
|
||||
{{ end }}
|
||||
|
||||
{{ if eq .Site.Params.search.engine 1 }}
|
||||
{{ if eq .Site.Params.search.engine 2 }}
|
||||
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.instantsearch.url $css.instantsearch.version) $css.instantsearch.sri | safeHTML }}
|
||||
{{ printf "<link rel=\"stylesheet\" href=\"%s\" integrity=\"%s\" crossorigin=\"anonymous\">" (printf $css.instantsearchTheme.url $css.instantsearchTheme.version) $css.instantsearchTheme.sri | safeHTML }}
|
||||
{{ end }}
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
{{ with $page.Content }}<p>{{ . | markdownify }}</p>{{ end }}
|
||||
|
||||
<div id="search-box">
|
||||
{{ if eq $.Site.Params.search.engine 1 }}
|
||||
<input name="q" id="search-query" placeholder="{{ i18n "search_placeholder" }}" autocapitalize="off" autocomplete="off" autocorrect="off" role="textbox" spellcheck="false" type="search">
|
||||
{{ else }}
|
||||
<!-- Search box will appear here -->
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div id="search-hits">
|
||||
@@ -20,7 +24,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="search-hit-template">
|
||||
{{ if eq $.Site.Params.search.engine 1 }}
|
||||
{{/* Fuse search result template. */}}
|
||||
<script id="search-hit-fuse-template" type="text/x-template">
|
||||
<div class="search-hit" id="summary-{{"{{key}}"}}">
|
||||
<div class="search-hit-content">
|
||||
<div class="search-hit-name">
|
||||
{{ printf "<a href=\"%s\">%s</a>" "{{relpermalink}}" "{{title}}" | safeHTML }}
|
||||
<div class="article-metadata search-hit-type">{{"{{type}}"}}</div>
|
||||
<p class="search-hit-description">{{"{{snippet}}"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{{ else }}
|
||||
{{/* Algolia search result template. */}}
|
||||
<script id="search-hit-algolia-template" type="text/html">
|
||||
<div class="search-hit">
|
||||
<div class="search-hit-content">
|
||||
<div class="search-hit-name">
|
||||
@@ -31,3 +50,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
{{ end }}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*************************************************
|
||||
* Academic: the website framework for Hugo.
|
||||
* https://github.com/gcushen/hugo-academic
|
||||
**************************************************/
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Configuration.
|
||||
* --------------------------------------------------------------------------- */
|
||||
|
||||
// Configure Fuse.
|
||||
let fuseOptions = {
|
||||
shouldSort: true,
|
||||
includeMatches: true,
|
||||
tokenize: true,
|
||||
threshold: 0.0,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 2,
|
||||
keys: [
|
||||
{name:'title', weight:0.8},
|
||||
{name:'summary', weight:0.6},
|
||||
{name:'content', weight:0.5},
|
||||
{name:'tags', weight:0.3}
|
||||
]
|
||||
};
|
||||
|
||||
// Configure summary.
|
||||
let summaryLength = 60;
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Functions.
|
||||
* --------------------------------------------------------------------------- */
|
||||
|
||||
// Get query from URI.
|
||||
function getSearchQuery(name) {
|
||||
return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
|
||||
}
|
||||
|
||||
// Set query in URI without reloading the page.
|
||||
function updateURL(url) {
|
||||
if (history.pushState) {
|
||||
window.history.pushState({path:url}, '', url);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-process new search query.
|
||||
function initSearch(force, fuse) {
|
||||
let query = $("#search-query").val();
|
||||
|
||||
// If query deleted, clear results.
|
||||
if ( query.length < 1)
|
||||
$('#search-hits').empty();
|
||||
|
||||
// Check for timer event (enter key not pressed) and query less than minimum length required.
|
||||
if (!force && query.length < fuseOptions.minMatchCharLength)
|
||||
return;
|
||||
|
||||
// Do search.
|
||||
$('#search-hits').empty();
|
||||
searchAcademic(query, fuse);
|
||||
let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + '?q=' + encodeURIComponent(query) + window.location.hash;
|
||||
updateURL(newURL);
|
||||
}
|
||||
|
||||
// Perform search.
|
||||
function searchAcademic(query, fuse) {
|
||||
let results = fuse.search(query);
|
||||
// console.log({"results": results});
|
||||
if (results.length > 0) {
|
||||
parseResults(query, results);
|
||||
} else {
|
||||
$('#search-hits').append('<div class="search-no-results">' + i18n.no_results + '</div>');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse search results.
|
||||
function parseResults(query, results) {
|
||||
$.each( results, function(key, value) {
|
||||
let content = value.item.content;
|
||||
let snippet = "";
|
||||
let snippetHighlights = [];
|
||||
|
||||
if ( fuseOptions.tokenize ) {
|
||||
snippetHighlights.push(query);
|
||||
} else {
|
||||
$.each( value.matches, function(matchKey, matchValue) {
|
||||
if (matchValue.key == "content") {
|
||||
let start = (matchValue.indices[0][0]-summaryLength>0) ? matchValue.indices[0][0]-summaryLength : 0;
|
||||
let end = (matchValue.indices[0][1]+summaryLength<content.length) ? matchValue.indices[0][1]+summaryLength : content.length;
|
||||
snippet += content.substring(start, end);
|
||||
snippetHighlights.push(matchValue.value.substring(matchValue.indices[0][0], matchValue.indices[0][1]-matchValue.indices[0][0]+1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (snippet.length < 1) {
|
||||
snippet += content.substring(0, summaryLength*2);
|
||||
}
|
||||
|
||||
// Load template.
|
||||
var template = $('#search-hit-fuse-template').html();
|
||||
|
||||
// Localize content types.
|
||||
let content_key = value.item.type;
|
||||
if (content_key in content_type) {
|
||||
content_key = content_type[content_key];
|
||||
}
|
||||
|
||||
// Parse template.
|
||||
let templateData = {
|
||||
key: key,
|
||||
title: value.item.title,
|
||||
type: content_key,
|
||||
relpermalink: value.item.relpermalink,
|
||||
snippet: snippet
|
||||
};
|
||||
let output = render(template, templateData);
|
||||
$('#search-hits').append(output);
|
||||
|
||||
// Highlight search terms in result.
|
||||
$.each( snippetHighlights, function(hlKey, hlValue){
|
||||
$("#summary-"+key).mark(hlValue);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function render(template, data) {
|
||||
// Replace placeholders with their values.
|
||||
let key, find, re;
|
||||
for (key in data) {
|
||||
find = '\\{\\{\\s*' + key + '\\s*\\}\\}'; // Expect placeholder in the form `{{x}}`.
|
||||
re = new RegExp(find, 'g');
|
||||
template = template.replace(re, data[key]);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Initialize.
|
||||
* --------------------------------------------------------------------------- */
|
||||
|
||||
// Wait for Fuse to initialize.
|
||||
$.getJSON( search_index_filename, function( search_index ) {
|
||||
let fuse = new Fuse(search_index, fuseOptions);
|
||||
|
||||
// On page load, check for search query in URL.
|
||||
if (query = getSearchQuery('q')) {
|
||||
$("#search-query").val(query);
|
||||
initSearch(true, fuse);
|
||||
}
|
||||
|
||||
// On search box key up, process query.
|
||||
$('#search-query').keyup(function (e) {
|
||||
clearTimeout($.data(this, 'searchTimer')); // Ensure only one timer runs!
|
||||
if (e.keyCode == 13) {
|
||||
initSearch(true, fuse);
|
||||
} else {
|
||||
$(this).data('searchTimer', setTimeout(function () {
|
||||
initSearch(false, fuse);
|
||||
}, 250));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user