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:
@@ -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
6
content/search/_index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: "Search"
|
||||
specialpost: true
|
||||
allpage: true
|
||||
summary: Search Page
|
||||
---
|
||||
31
layouts/index.searchindex.json
Normal file
31
layouts/index.searchindex.json
Normal 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 -}}
|
||||
]
|
||||
@@ -33,6 +33,9 @@
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
{{ partial "search-form.html" . }}
|
||||
</li>
|
||||
<li class="menu-separator">
|
||||
<span>|</span>
|
||||
</li>
|
||||
|
||||
6
layouts/partials/search-form.html
Normal file
6
layouts/partials/search-form.html
Normal 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>
|
||||
177
layouts/partials/search-index.html
Normal file
177
layouts/partials/search-index.html
Normal 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
23
layouts/search/list.html
Normal 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
7
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
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
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
71
static/js/search.js
Normal 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
2
static/js/search.min.js
vendored
Normal 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}
|
||||
Reference in New Issue
Block a user