Implementeren zoekfunctionaliteit

This commit is contained in:
Michael Boelen 2023-05-18 13:03:01 +02:00
parent 55c242d4b6
commit 372746317e
16 changed files with 247 additions and 13 deletions

9
assets/js/fuse.js Normal file

File diff suppressed because one or more lines are too long

7
assets/js/mark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
assets/js/scripts.json Normal file
View file

@ -0,0 +1,6 @@
{
"scripts": [
"js/fuse.js",
"js/mark.min.js"
]
}

View file

@ -15,6 +15,10 @@
mediaType = "text/calendar" mediaType = "text/calendar"
baseName = "calendar" baseName = "calendar"
[SearchIndex]
mediatype = "application/json"
basename = "searchindex"
#[outputFormats.XMLEvent] #[outputFormats.XMLEvent]
# mediaType = "application/xml" # mediaType = "application/xml"
# baseName = "schedule" # baseName = "schedule"

View file

@ -2,7 +2,7 @@
# Voor de home-page maken we een HTML, RSS en JSON Feed # Voor de home-page maken we een HTML, RSS en JSON Feed
# Secties alleen in HTML en voor pagina's in zowel HTML als CalendarEvent (iCAL) waar het van toepassing is # Secties alleen in HTML en voor pagina's in zowel HTML als CalendarEvent (iCAL) waar het van toepassing is
home = ["HTML", "RSS", "JSON"] home = ["HTML", "RSS", "JSON", "SearchIndex"]
section = ["HTML"] section = ["HTML"]
page = ["HTML", "CalendarEvent"] page = ["HTML", "CalendarEvent"]

9
content/zoeken/index.md Normal file
View file

@ -0,0 +1,9 @@
---
title: "Zoekresultaten"
sitemap:
priority : 0.1
layout: "search"
---
<!-- De content van deze pagina zal niet zichtbaar zijn. Deze pagina is aanwezig zodat die reageert op requests voor /zoeken/ -->

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>

After

Width:  |  Height:  |  Size: 481 B

141
static/js/search.js Normal file
View file

@ -0,0 +1,141 @@
// MB: created at 2023-04-24
summaryInclude = 50;
var fuseOptions = {
shouldSort: true,
includeMatches: true,
includeScore: true,
tokenize: true,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [
{name: "title", weight: 0.45},
{name: "content", weight: 0.4},
{name: "tags", weight: 0.1},
{name: "categories", weight: 0.05}
]
};
// =============================
// Search
// =============================
var inputBox = document.getElementById('search-query');
if (inputBox !== null) {
var searchQuery = param("q");
if (searchQuery) {
inputBox.value = searchQuery || "";
executeSearch(searchQuery, false);
} else {
document.getElementById('search-results').innerHTML = '<p class="search-results-empty">Typ een woord om te zoeken of bekijk alle <a href="/tags/">tags</a>.</p>';
}
}
function executeSearch(searchQuery) {
show(document.querySelector('.search-loading'));
fetch('/searchindex.json').then(function (response) {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' + response.status);
return;
} else {
console.log('/searchindex.json loaded');
}
// Examine the text in the response
response.json().then(function (pages) {
var fuse = new Fuse(pages, fuseOptions);
var result = fuse.search(searchQuery);
if (result.length > 0) {
populateResults(result);
} else {
document.getElementById('search-results').innerHTML = '<p class=\"search-results-empty\">No matches found</p>';
}
hide(document.querySelector('.search-loading'));
})
.catch(function (err) {
console.log('Fetch Error :-S', err);
});
});
}
function populateResults(results) {
var searchQuery = document.getElementById("search-query").value;
var searchResults = document.getElementById("search-results");
// pull template from hugo template definition
var templateDefinition = document.getElementById("search-result-template").innerHTML;
results.forEach(function (value, key) {
var content = value.item.content;
var snippet = "";
var snippetHighlights = [];
snippetHighlights.push(searchQuery);
snippet = content.substring(0, summaryInclude * 2) + '&hellip;';
//replace values
var tags = ""
if (value.item.tags) {
value.item.tags.forEach(function (element) {
tags = tags + "<a href='/tags/" + element + "'>" + "#" + element + "</a> "
});
}
var output = render(templateDefinition, {
key: key,
title: value.item.title,
link: value.item.url,
tags: tags,
categories: value.item.categories,
snippet: snippet
});
searchResults.innerHTML += output;
snippetHighlights.forEach(function (snipvalue, snipkey) {
var instance = new Mark(document.getElementById('summary-' + key));
instance.mark(snipvalue);
});
});
}
function render(templateString, data) {
var conditionalMatches, conditionalPattern, copy;
conditionalPattern = /\$\{\s*isset ([a-zA-Z]*) \s*\}(.*)\$\{\s*end\s*}/g;
//since loop below depends on re.lastInxdex, we use a copy to capture any manipulations whilst inside the loop
copy = templateString;
while ((conditionalMatches = conditionalPattern.exec(templateString)) !== null) {
if (data[conditionalMatches[1]]) {
//valid key, remove conditionals, leave content.
copy = copy.replace(conditionalMatches[0], conditionalMatches[2]);
} else {
//not valid, remove entire section
copy = copy.replace(conditionalMatches[0], '');
}
}
templateString = copy;
//now any conditionals removed we can do simple substitution
var key, find, re;
for (key in data) {
find = '\\$\\{\\s*' + key + '\\s*\\}';
re = new RegExp(find, 'g');
templateString = templateString.replace(re, data[key]);
}
return templateString;
}
// Helper Functions
function show(elem) {
elem.style.display = 'block';
}
function hide(elem) {
elem.style.display = 'none';
}
function param(name) {
return decodeURIComponent((location.search.split(name + '=')[1] || '').split('&')[0]).replace(/\+/g, ' ');
}

View file

@ -8,7 +8,6 @@
<div class="content"> <div class="content">
{{ partialCached "header.html" . }} {{ partialCached "header.html" . }}
{{ partial "breadcrumb.html" . }} {{ partial "breadcrumb.html" . }}
<section>
<!-- <h2 class="post">{{ .Title }}</h2> --> <!-- <h2 class="post">{{ .Title }}</h2> -->
{{- block "main" . }} {{- block "main" . }}
{{ .Content }} {{ .Content }}
@ -19,7 +18,7 @@
{{ if isset .Params "show_child_pages" }} {{ if isset .Params "show_child_pages" }}
{{ if eq .Params.show_child_pages true }} {{ if eq .Params.show_child_pages true }}
<section> <section>
<p>Relevante pagina's:</p> <h3>Gerelateerde pagina's</h3>
<ul> <ul>
{{ range .Pages }} {{ range .Pages }}
<li> <li>
@ -32,19 +31,23 @@
{{ end }} {{ end }}
{{ if eq .Section "posts" }} {{ if eq .Section "posts" }}
<div class="post-date"> <section>
<span class="g time">{{.Date.Format "January 2, 2006"}} </span> &#8729; <h3>Tags</h3>
{{ $taxonomy := "tags" }} {{ with .Param $taxonomy }} <div class="post-date">
{{ range $index, $tag := . }} {{ with $.Site.GetPage (printf "/%s/%s" <span class="g time">{{.Date.Format "January 2, 2006"}} </span> &#8729;
$taxonomy $tag) -}} {{ $taxonomy := "tags" }} {{ with .Param $taxonomy }}
<a href="{{ .Permalink }}">{{ $tag | urlize }}</a> {{ range $index, $tag := . }} {{ with $.Site.GetPage (printf "/%s/%s" $taxonomy $tag) -}}
{{- end -}} {{- end -}} <a href="{{ .Permalink }}">{{ $tag | urlize }}</a>
{{ end }} {{- end -}} {{- end -}}
</div> {{ end }}
{{ end }} </div>
</section> </section>
{{ end }}
</div> </div>
</main> </main>
{{ partial "footer.html" . }} {{ partial "footer.html" . }}
</body> </body>
{{ partialCached "scripts_loadlast.html" . }}
</html> </html>

View file

@ -0,0 +1,3 @@
index.json
Genereert een index (/index.json) ten behoeve van zoekfunctionaliteit

View file

@ -0,0 +1,26 @@
{{ define "main" }}
<h2>Zoeken</h2>
{{ partial "search-form.html" . }}
<div class="container">
<div id="search-results"></div>
<div class="search-loading">Wachten op zoekopdracht of resulten... (JavaScript is voor deze pagina vereist)</div>
<!-- this template is sucked in by search.js and appended to the search-results div above. So editing here will adjust style -->
<script id="search-result-template" type="text/x-js-template">
<div id="summary-${key}">
<h3><a href="${link}">${title}</a></h3>
<p>${snippet}</p>
<p>
<small>
${ isset tags }Tags: ${tags}<br>${ end }
</small>
</p>
</div>
</script>
</div>
{{ end }}

View file

@ -0,0 +1,5 @@
{{ $index := slice }}
{{ range .Site.RegularPages }}
{{ $index = $index | append (dict "title" .Title "tags" .Params.tags "categories" .Params.categories "content" .Plain "url" .Permalink) }}
{{ end }}
{{ $index | jsonify }}

View file

@ -30,5 +30,6 @@
{{ end }} {{ end }}
</ul> </ul>
</nav> </nav>
<a href="/zoeken/">{{ partial "show-svg-icon.html" (dict "context" . "icon" "magnifying-glass") }}</a>
</div> </div>
</header> </header>

View file

@ -0,0 +1,15 @@
<script src="{{ "js/search.js" | absURL }}"></script>
{{ $assetBusting := not .Site.Params.disableAssetsBusting }}
{{ $scripts := getJSON "assets/js/scripts.json" }}
{{ $.Scratch.Set "jslibs" slice }}
{{ range $scripts.scripts }}
{{ $.Scratch.Add "jslibs" (resources.Get . ) }}
{{ end }}
{{ $js := .Scratch.Get "jslibs" | resources.Concat "js/combined-scripts.js" | resources.Minify | fingerprint }}
<script
src="{{ $js.RelPermalink }}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}"
integrity="{{ $js.Data.Integrity }}"
></script>

View file

@ -0,0 +1,4 @@
<form action="/zoeken/" method="GET">
🔍 <input type="search" name="q" id="search-query" placeholder="Zoekterm..">
<button type="submit">Zoek</button>
</form>