Showcase
You can embed the search everywhere you like 😉
Overview
To implement a full text search in my hugo site, I used a method I found in a post from weitblick.org and modified it to my needs.
It’s really simple:
- create index.json template
- modify config.toml
- add search.js to website
- create search.html and modify it if needed
- create search.md page
1. index.json
add index.json to ./layouts/_default
[ {{- $i := 0 -}}
{{- range where .Site.RegularPages "Section" "ne" "" -}}
{{- if not .Params.noSearch -}}
{{- if gt $i 0 }},{{ end -}}
{"date":"{{ .Date.Unix }}", "url":"{{ .Permalink }}", "title":{{ .Title | jsonify }}, "summary":{{ with .Description}}{{ . | plainify | jsonify }}{{ else }}{{ .Summary | plainify | jsonify }}{{ end }}, "content":{{ .Content | plainify | jsonify }},"tags":[ {{- $t := 0 }}{{- range .Param "tags" -}}{{ if gt $t 0 }},{{ end }}{{ . | jsonify }}{{ $t = add $t 1 }}{{ end -}} ], "section": {{ .Section | jsonify -}} }
{{- $i = add $i 1 -}}
{{- end -}}
{{- end -}} ]
2. config.toml
add to config.toml
[outputs]
home = [ "HTML", "JSON" ]
In my case, I use yaml:
outputs:
home:
- HTML
- JSON
3. search.js
add search.js to ./static/js/
// JSON based simple full text search with vanilla javascript via XMLHttpRequest,
// (c) 2020 by Tanja Becker - Webdeveleopment, http://tanjabecker.de/
// pass query string by CGI parameter: [URL]?query=[search string]
var query = new URLSearchParams(window.location.search).get("query");
if ((query != '') && (query != null)) {
document.getElementById("custom-search-field").value = query;
}
// Debug mode: shows number of hits in the search result. [URL]?debug=1
let debug = new URLSearchParams(window.location.search).get("debug");
let merge_data = [];
window.addEventListener('load', function() {
document.getElementById("custom-search").querySelectorAll("input").forEach(item => {
item.disabled = 'disabled';
});
if (!merge_data.length) {
document.getElementById("custom-search-results").innerHTML = params['json_wait'];
if (params['json_src'] != '') {
var json_src_str = params['json_src'].replace("/\s/g", "");
var json_sources = json_src_str.split(",");
var src_count = 0;
for (var i = 0; i < json_sources.length; i++) {
var xmlhttp = new XMLHttpRequest();
var url = json_sources[i];
xmlhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var data = JSON.parse(this.responseText);
for (var i = 0; i < data.length; i++) {
merge_data.push(data[i]);
}
src_count++;
if (src_count == json_sources.length) {
document.getElementById("custom-search").querySelectorAll("input").forEach(item => {
item.disabled = '';
});
document.getElementById("custom-search-field").focus();
document.getElementById("custom-search-results").innerHTML = params['json_ready'];
if ((query != '') && (query != null)) {
customSearchResults();
}
}
}
if (this.status == 404) {
document.getElementById("custom-search-results").innerHTML = params['err_filefailed'];
}
};
xmlhttp.open("GET", url, true);
xmlhttp.send();
}
}
}
}, false);
document.getElementById("custom-search-field").addEventListener('keyup', function(e) {
if (params['autocomplete'] == 1) {
document.getElementById("custom-search-results").innerHTML = '';
if (this.value == '') {
document.getElementById("custom-search-results").innerHTML = params['err_nostring'];
return false;
} else if (this.value.length < params['minlength']) {
document.getElementById("custom-search-results").innerHTML = params['err_badstring'];
return false;
} else if (this.value.length >= params['minlength']) {
customSearchResults();
}
} else {
if (this.value == '') {
document.getElementById("custom-search-results").innerHTML = params['err_nostring'];
}
}
});
let sForm = document.getElementById("custom-search");
if (params['defaultsearch'] != '') {
sForm.querySelectorAll("input[name='option']").forEach(item => {
item.addEventListener('click', event => {
customSearchResults();
});
});
}
let sSection = '';
if ((params['section_search'] == 1) && (sForm.elements["section[]"])) {
sSection = sForm.elements["section[]"];
sForm.querySelectorAll("input[name='section[]']").forEach(item => {
item.addEventListener('click', event => {
if (item.value == 'site') {
if (item.checked) {
sForm.querySelectorAll("input[name='section[]']").forEach(node => {
node.checked = false;
});
sForm.querySelector("input[value='site']").checked = true;
customSearchResults();
}
} else {
if (item.checked) {
sForm.querySelector("input[value='site']").checked = false;
} else {
var section_checked = [];
for (var i = 0; i < sSection.length; i++) {
if ((sSection[i].checked) && (sSection[i].value != 'site')) {
section_checked.push(sSection[i].value);
} else {
continue;
}
}
if (section_checked.length < 1) sForm.querySelector("input[value='site']").checked = true;
}
}
if (document.getElementById("custom-search-field").value != '') {
customSearchResults();
}
});
});
}
function customSearchResults() {
if (!merge_data.length) {
return false;
}
var sOutput = document.getElementById("custom-search-results");
var sString = document.getElementById("custom-search-field").value;
if (sString == '') {
sOutput.innerHTML = params['err_nostring'];
return false;
}
if (sString.length < params['minlength']) {
sOutput.innerHTML = params['err_badstring'];
return false;
}
var data = merge_data;
var add_searchlink = params['add_searchlink'];
if ((add_searchlink) && (add_searchlink != '')) {
add_searchlink = add_searchlink.replace(/\[QUERY\]/g, sString);
}
var sOption = '';
var optionField = document.forms["custom-search"]["option"];
if (optionField) {
sOption = optionField.value;
} else {
sOption = params['defaultsearch'];
}
var badwords = [];
if (params['badwords'] != '') {
var badwords_str = params['badwords'];
badwords_str = badwords_str.replace("/\s/g", "");
badwords = badwords_str.split(",");
}
var words = [];
var search_words = sString.split(" ");
for (var i = 0; i < search_words.length; i++) {
if (badwords.includes(search_words[i])) {
continue;
} else {
words.push(search_words[i]);
}
}
if (!words.length) {
sOutput.innerHTML = params['err_badstring'];
return false;
}
var section_filter = [];
if (sSection != '') {
for (var i = 0; i < sSection.length; i++) {
if ((sSection[i].checked) && (sSection[i].value != 'site')) {
section_filter.push(sSection[i].value);
} else {
continue;
}
}
}
var results = [];
for (var i = 0; i < data.length; i++) {
var title = data[i].title;
var summary = data[i].summary;
var content = data[i].content;
var tags = data[i].tags;
var section = data[i].section;
if (section_filter.length >= 1) {
if (!section_filter.includes(section)) {
continue;
}
}
var searchtext = '';
if (title != '') searchtext += title;
if (summary != '') searchtext += ' '+summary;
if (content != '') searchtext += ' '+content;
if (tags != '') searchtext += ' '+tags;
var matched = 0;
var matches = 0;
var matches_calc = 0;
var title_matches = 0;
var tags_matches = 0;
var summary_matches = 0;
var content_matches = 0;
for (var y = 0; y < words.length; y++) {
var searchword = new RegExp(words[y], 'gi');
if (searchword.test(searchtext) === true) {
var wordmatches = searchtext.match(searchword);
matches = matches + wordmatches.length;
matches_calc = matches;
for (var key in searchfield_weight) {
var val = searchfield_weight[key];
var searchstr = '';
var count = '';
if (key == 'title') searchstr = title;
if ((key == 'tags') && (tags != '')) searchstr = tags.join(",");
if (key == 'summary') searchstr = summary;
if (key == 'content') searchstr = content;
if (searchword.test(searchstr) === true) {
matches_calc = matches_calc + val;
count = searchstr.match(searchword).length;
if (key == 'title') title_matches = title_matches + count;
if ((key == 'tags') && (tags != '')) tags_matches = tags_matches + count;
if (key == 'summary') summary_matches = summary_matches + count;
if (key == 'content') content_matches = content_matches + count;
}
}
matched++;
}
}
data[i]['matches_calc'] = matches_calc;
if ((debug != '') && (debug != null)) {
data[i]['matches'] = matches;
data[i]['title_matches'] = title_matches;
data[i]['tags_matches'] = tags_matches;
data[i]['summary_matches'] = summary_matches;
data[i]['content_matches'] = content_matches;
}
if (sOption == 'OR') {
if (matched >= 1) {
results.push(data[i]);
}
} else {
if (matched == words.length) {
results.push(data[i]);
}
}
}
// results
if (results.length >= 1) {
results.sort(function(a, b) {
if (params['sort_date'] == 'DESC') {
return b.matches_calc - a.matches_calc || b.date - a.date;
} else {
return b.matches_calc - a.matches_calc || a.date - b.date;
}
});
var results_header = '';
if (results.length > 1) {
results_header = params['res_more_items'];
} else {
results_header = params['res_one_item'];
}
results_header = results_header.replace("[CNT]", results.length);
if ((add_searchlink) && (add_searchlink != '')) {
results_header += add_searchlink;
}
var tag_top = params['res_out_top'];
var tag_bottom = params['res_out_bottom'];
var results_content = '';
for (var i = 0; i < results.length; i++) {
var title = results[i].title;
var summary = results[i].summary;
var date = results[i].date;
var url = results[i].url;
var tags = results[i].tags;
var section = results[i].section;
var extern = results[i].extern;
// only for debug mode
var infos = '';
if ((debug != '') && (debug != null)) {
var matches = results[i].matches;
var matches_calc = results[i].matches_calc;
var title_matches = results[i].title_matches;
var tags_matches = results[i].tags_matches;
var summary_matches = results[i].summary_matches;
var content_matches = results[i].content_matches;
var plus_title = title_matches >=1 ? '+ '+searchfield_weight['title'] : '+ 0';
var plus_tags = tags_matches >=1 ? '+ '+searchfield_weight['tags'] : '+ 0';
var plus_summary = summary_matches >=1 ? '+ '+searchfield_weight['summary'] : '+ 0';
var plus_content = content_matches >=1 ? '+ '+searchfield_weight['content'] : '+ 0';
infos += '<table style="font-size: 12px; color: #666666;">';
infos += '<tr><td style="width: 100px;"><b>Gesamt:</b></td><td style="text-align: right;">'+matches+'</td></tr>';
infos += '<tr><td>Title ('+title_matches+'):</td><td style="text-align: right;">'+plus_title+'</td></tr>';
infos += '<tr><td>Tags ('+tags_matches+'):</td><td style="text-align: right;">'+plus_tags+'</td></tr>';
infos += '<tr><td>Summary ('+summary_matches+'):</td><td style="text-align: right;">'+plus_summary+'</td></tr>';
infos += '<tr><td>Content ('+content_matches+'):</td><td style="text-align: right;">'+plus_content+'</td></tr>';
infos += '<tr><td><b>Gesamt neu:</b></td><td style="text-align: right;">'+matches_calc+'</td></tr>';
infos += '</table><br>';
}
if ((extern == 1) && (params['extern_icon'])) {
title += ' '+params['extern_icon'];
url = url + '" target="_blank"';
}
if (section != '') {
if (section_trans[section] != undefined) {
section = section_trans[section];
}
}
var newdate = new Date(date*1000);
var datestr = newdate.toLocaleDateString();
var templ = params['res_item_tpl'];
if (templ) {
templ = templ.replace("[TITLE]", title);
templ = templ.replace(/\[URL\]/g, url);
if (templ.includes("[DATE]")) {
if (date != '') {
templ = templ.replace("[DATE]", datestr);
} else {
templ = templ.replace("[DATE]", '');
}
}
if (templ.includes("[SUMMARY]")) {
if (summary != '') {
templ = templ.replace("[SUMMARY]", summary);
} else {
templ = templ.replace("[SUMMARY]", '');
}
}
if (templ.includes("[TAGS]")) {
if (tags != '') {
tags = tags.join(",");
tags = tags.replace(/,/g, ", ");
templ = templ.replace("[TAGS]", tags);
} else {
templ = templ.replace("[TAGS]", '');
}
}
if (templ.includes("[SECTION]")) {
if (section != '') {
templ = templ.replace("[SECTION]", '['+section+'] ');
} else {
templ = templ.replace("[SECTION]", '');
}
}
results_content += templ;
if (infos != '') {
results_content += '<li>'+infos+'</li>';
}
}
}
sOutput.innerHTML = results_header+tag_top+results_content+tag_bottom;
} else {
var noresult = '';
if ((add_searchlink) && (add_searchlink != '')) {
noresult += add_searchlink;
}
noresult += params['err_noresult'];
sOutput.innerHTML = noresult;
}
}
4. search.html
add search.html to ./layouts/shortcodes/
<form id="custom-search" name="custom-search" method="post" action="" onsubmit="customSearchResults(); return false;">
<p>
<input id="custom-search-field" type="text" name="search" value="" title="Search String" placeholder="Search">
<!-- <input type="submit" value="Suchen"> -->
</p>
<!-- <p><em>durchsuchen:</em><br>
<input type="checkbox"name="section[]" value="site" checked="checked"> alles<br>
<input type="checkbox" name="section[]" value="post"> blog<br>
<input type="checkbox" name="section[]" value="other-section"> other section
</p> -->
<!-- <p> -->
<!-- <input type="radio" name="option" value="AND" checked="checked"> UND-Suche<br> -->
<!-- <input type="radio" name="option" value="OR"> ODER-Suche -->
<!-- </p> -->
</form>
<div id="custom-search-results"></div>
<script>
// CUSTOM AREA
let params = {
json_src : '/index.json', // for multiple sources: comma separated list of JSONarrays
minlength : 3,
defaultsearch : 'AND',
sort_date : 'DESC',
autocomplete : 1, // 0: form needs a submit button
section_search : 0, // 1: needs checkboxes with name="section[]"
badwords : 'and,or,but,if,so,the,that,one,of', //ignore this words
json_wait : '<p><em>Searching...</em></p>',
json_ready : '<p><em>Please enter your search keywords.</em></p>',
extern_icon : ' (externer Link)', // marker for external links (optional)
err_badstring : '<p><em>Search term too short!</em></p>',
err_noresult : '<p><em>Sorry, no search result. Please try again.</em></p>',
err_norequest : '<p style="text-align: center; color:red;">The full text search is currently not available.</p>',
err_nostring : '<p><em>Please enter your search keywords.</em></p>',
err_filefailed : '<p style="text-align: center;color: red;">A file could not be retrieved.</p>',
res_one_item : '<p><em>[CNT] SEARCH RESULT</em></p>',
res_more_items : '<p><em>[CNT] SEARCH RESULTS</em></p>',
res_out_top : '<ul>',
res_out_bottom : '</ul>',
res_item_tpl : '<li><a href="[URL]">[TITLE]</a><br>[DATE]:[SUMMARY]<br><em>[SECTION][TAGS]</em></li>',
<!-- add_searchlink : '<p><a href="https://duckduckgo.com/?q=site:yourdomain.com [QUERY]" target="_blank"><i>Nicht zufrieden mit den Suchergebnissen? Externe Suche via DuckDuckGo ...</i></a></p>' -->
};
// Translation of section name (optional)
let section_trans = {
"post" : "Blog",
// "other-section" : "Other Section"
};
let searchfield_weight = {
"title" : 5,
"tags" : 5,
"summary" : 2,
"content" : 1
};
// CUSTOM AREA END
</script>
<script type="text/javascript" src="/js/search.js"></script>
5. search.md
add search.md page
---
title: "Search"
summary: "Search blog for keywords"
toc: false
socialShare: false
date: false
---
{{< search >}}