Add Lunr.js for client-side search functionality and implement search results display

- Integrated Lunr.js library for indexing and searching content.
- Created search.js to handle search queries and display results dynamically.
- Implemented displayResults function to format and show search results.
- Enhanced search functionality with boosted fields for title and content.
- Added search.min.js for optimized production use.
This commit is contained in:
Fundor333
2025-04-18 22:26:29 +02:00
parent bf58f5cd37
commit 5acba397b4
13 changed files with 3861 additions and 2 deletions

View File

@@ -32,10 +32,16 @@ related:
- name: date
weight: 10
outputFormats:
SearchIndex:
baseName: search
mediaType: application/json
outputs:
home:
- html
- rss
- SearchIndex
services:
rss:

6
content/search/_index.md Normal file
View File

@@ -0,0 +1,6 @@
---
title: "Search"
specialpost: true
allpage: true
summary: Search Page
---

View File

@@ -0,0 +1,31 @@
{{ $configDateFormat := .Site.Params.dateFormat | default "2 Jan 2006" }}
[
{{- range $index, $page := .Site.RegularPages -}}
{{- if gt $index 0 -}}
,
{{- end -}}
{{- $entry := dict "uri" $page.RelPermalink "title" $page.Title "content" ($page.Plain | htmlUnescape) -}}
{{- if $page.Params.subtitle -}}
{{- $subtitle := partial "utils/markdownify.html" (dict "$" $page "raw" $page.Params.subtitle "isContent" false) -}}
{{- $entry = merge $entry (dict "subtitle" ($subtitle | plainify)) -}}
{{- end -}}
{{- if $page.Params.date -}}
{{- $entry = merge $entry (dict "date" ($page.Params.date | time.Format $configDateFormat )) -}}
{{- end -}}
{{- if .Site.Params.displayPostDescription -}}
{{- $description := partial "utils/markdownify.html" (dict "$" $page "raw" $page.Description "isContent" false) -}}
{{- $entry = merge $entry (dict "description" ($description | plainify)) -}}
{{- end -}}
{{- if .Site.Params.displayCategory -}}
{{- if eq .Site.Params.categoryBy "sections" -}}
{{- $entry = merge $entry (dict "categories" (slice $page.Section)) -}}
{{- else if eq .Site.Params.categoryBy "categories" -}}
{{- $entry = merge $entry (dict "categories" $page.Params.categories) -}}
{{- end -}}
{{- end -}}
{{- if .Site.Params.enablePostTags -}}
{{- $entry = merge $entry (dict "tags" $page.Params.tags) -}}
{{- end -}}
{{- $entry | jsonify -}}
{{- end -}}
]

View File

@@ -33,6 +33,9 @@
{{ end }}
</li>
{{ end }}
<li>
{{ partial "search-form.html" . }}
</li>
<li class="menu-separator">
<span>|</span>
</li>

View File

@@ -0,0 +1,6 @@
<form id="search"
action='{{ with .GetPage "/search" }}{{.Permalink}}{{end}}' method="get">
<label hidden for="search-input">Search site</label>
<input type="text" id="search-input" name="q"
placeholder="">
</form>

View File

@@ -0,0 +1,177 @@
<template id="search-result" hidden>
<article class="content post post-item">
<h4 class="post-title post-item-title"><a class="read-more-link"><i class="fa-regular fa-newspaper"></i> <a class="summary-title-link"></a></a></h4>
<summary class="summary" hidden></summary>
<div class="read-more-container">
<time class="post-item-meta"></time>
</div>
</article>
</template>
<script>
window.addEventListener("DOMContentLoaded", function(event)
{
var index = null;
var lookup = null;
var queuedTerm = null;
var form = document.getElementById("search");
var input = document.getElementById("search-input");
const params = new URLSearchParams(window.location.search)
const query = params.get('q')
startSearch(query)
form.addEventListener("submit", function(event)
{
event.preventDefault();
var term = input.value.trim();
if (!term)
return;
startSearch(term);
}, false);
function startSearch(term)
{
// Start icon animation.
form.setAttribute("data-running", "true");
if (index)
{
// Index already present, search directly.
search(term);
}
else if (queuedTerm)
{
// Index is being loaded, replace the term we want to search for.
queuedTerm = term;
}
else
{
// Start loading index, perform the search when done.
queuedTerm = term;
initIndex();
}
}
function searchDone()
{
// Stop icon animation.
form.removeAttribute("data-running");
queuedTerm = null;
}
function initIndex()
{
var request = new XMLHttpRequest();
request.open("GET", "/search.json");
request.responseType = "json";
request.addEventListener("load", function(event)
{
lookup = {};
index = lunr(function()
{
// Uncomment the following line and replace de by the right language
// code to use a lunr language pack.
// this.use(lunr.de);
this.ref("uri");
// If you added more searchable fields to the search index, list them here.
this.field("title");
this.field("content");
this.field("description");
this.field("categories");
this.field("date");
for (var doc of request.response)
{
this.add(doc);
lookup[doc.uri] = doc;
}
});
// Search index is ready, perform the search now
search(queuedTerm);
}, false);
request.addEventListener("error", searchDone, false);
request.send(null);
}
function search(term)
{
var results = index.search(term);
// The element where search results should be displayed, adjust as needed.
var target = document.querySelector(".main-inner");
while (target.firstChild)
target.removeChild(target.firstChild);
var title = document.createElement("h1");
title.id = "search-results";
title.className = "list-title";
if (results.length == 0)
title.textContent = `No results found for “${term}`;
else if (results.length == 1)
title.textContent = `Found one result for “${term}`;
else
title.textContent = `Found ${results.length} results for “${term}`;
target.appendChild(title);
document.title = title.textContent;
var template = document.getElementById("search-result");
for (var result of results)
{
var doc = lookup[result.ref];
// Fill out search result template, adjust as needed.
var element = template.content.cloneNode(true);
element.querySelector(".summary-title-link").href =
element.querySelector(".read-more-link").href = doc.uri;
element.querySelector(".summary-title-link").textContent = doc.title;
element.querySelector(".summary").textContent = truncate(doc.content, 70);
element.querySelector(".post-item-meta").textContent = doc.date;
target.appendChild(element);
}
title.scrollIntoView(true);
searchDone();
}
// This matches Hugo's own summary logic:
// https://github.com/gohugoio/hugo/blob/b5f39d23b8/helpers/content.go#L543
function truncate(text, minWords)
{
var match;
var result = "";
var wordCount = 0;
var regexp = /(\S+)(\s*)/g;
while (match = regexp.exec(text))
{
wordCount++;
if (wordCount <= minWords)
result += match[0];
else
{
var char1 = match[1][match[1].length - 1];
var char2 = match[2][0];
if (/[.?!"]/.test(char1) || char2 == "\n")
{
result += match[1];
break;
}
else
result += match[0];
}
}
return result;
}
}, false);
</script>

23
layouts/search/list.html Normal file
View File

@@ -0,0 +1,23 @@
{{ define "main"}}
<div class="wrapper list-page">
<header class="header">
<h1 class="header-title center">{{ .Title }}</h1>
</header>
<main class="page-content" aria-label="Content">
<div class="wrapper">
<div class="main-inner">
<script src="/js/lunr.min.js"></script>
</div>
</div>
{{ partial "search-index.html" . }}
</main>
</div>
{{ end }}

7
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"autoprefixer": "^10.4.21",
"cjs-loader": "^0.1.0",
"lunr": "^2.3.9",
"postcss": "^8.5.1",
"postcss-cli": "^11.0.1"
},
@@ -415,6 +416,12 @@
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",

View File

@@ -6,6 +6,7 @@
"dependencies": {
"autoprefixer": "^10.4.21",
"cjs-loader": "^0.1.0",
"lunr": "^2.3.9",
"postcss": "^8.5.1",
"postcss-cli": "^11.0.1"
},
@@ -14,8 +15,7 @@
"node": "16.x",
"npm": "8.x"
},
"keywords": [
],
"keywords": [],
"name": "fundor333.com",
"private": true,
"version": "0.0.0"

3475
static/js/lunr.js Normal file

File diff suppressed because it is too large Load Diff

52
static/js/lunr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

71
static/js/search.js Normal file
View File

@@ -0,0 +1,71 @@
function displayResults (results, store) {
// grab the element that we will use to place our search results
const searchResults = document.getElementById('results')
// if the result(s) found
if (results.length) {
let resultList = ''
// iterate over the results
for (const n in results) {
// get the data
const item = store[results[n].ref]
// build result list elements
// if you want to style the list, edit this
resultList += `
<li>
<h2>
<a href="${item.url}">${item.title}</a>
</h2>'
<p>${item.content.substring(0, 150)}...</p>
<p>${item.tags}</p>
</li>
`;
}
// place the result list
searchResults.innerHTML = resultList
} else {
// if no result return this feedback
searchResults.innerHTML = 'No results found.'
}
}
// Get the query parameter(s)
const params = new URLSearchParams(window.location.search)
const query = params.get('query')
// if query exists, perform the search
if (query) {
// Retain the query in the search form after redirecting to the search page
document.getElementById('search-input').setAttribute('value', query)
// Search these fields
const idx = lunr(function () {
this.ref('id')
this.field('title', {
// boost search to 15
boost: 15
})
this.field('tags')
this.field('content', {
// boost search to 10
boost: 10
})
// provide search index data to lunr function / idx
for (const key in window.store) {
this.add({
id: key,
title: window.store[key].title,
tags: window.store[key].category,
content: window.store[key].content
})
}
})
// Perform the search
const results = idx.search(query)
// get the result and build the result list
displayResults(results, window.store)
// Replace the title to 'Search Results for <query>' so user know if the search is successful
document.getElementById('search-title').innerText = 'Search Results for ' + query
}

2
static/js/search.min.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// @ts-nocheck
function displayResults(t,e){const n=document.getElementById("results");if(t.length){let s="";for(const n in t){const i=e[t[n].ref];s+=`\n\t\t<li>\n\t\t <h2>\n\t\t\t<a href="${i.url}">${i.title}</a>\n\t\t </h2>'\n\t\t <p>${i.content.substring(0,150)}...</p>\n\t\t <p>${i.tags}</p>\n\t\t</li>\n\t `}n.innerHTML=s}else n.innerHTML="No results found."}const params=new URLSearchParams(window.location.search),query=params.get("query");if(query){document.getElementById("search-input").setAttribute("value",query);displayResults(lunr((function(){this.ref("id"),this.field("title",{boost:15}),this.field("tags"),this.field("content",{boost:10});for(const t in window.store)this.add({id:t,title:window.store[t].title,tags:window.store[t].category,content:window.store[t].content})})).search(query),window.store),document.getElementById("search-title").innerText="Search Results for "+query}