Add site search with lunr.js
This commit is contained in:
parent
42d5f4bb73
commit
2c604dafcb
14 changed files with 224 additions and 10 deletions
|
@ -7,6 +7,6 @@
|
|||
"globals": {
|
||||
"mermaid": true,
|
||||
"$": true,
|
||||
"process": true
|
||||
"URLSearchParams": true
|
||||
}
|
||||
}
|
||||
|
|
7
content/search.md
Normal file
7
content/search.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Search
|
||||
layout: search
|
||||
outputs: [json, html]
|
||||
image: unsplash:mdS647i6-ZM
|
||||
hidden_from_navbar: true
|
||||
---
|
|
@ -41,6 +41,8 @@
|
|||
|
||||
{{ partial "script_async.html" "js/app.js" }}
|
||||
|
||||
{{ block "scripts_extra" . }}{{ end }}
|
||||
|
||||
{{ if not .Site.IsServer }}
|
||||
<script async defer data-domain="theorangeone.net" src="https://elbisualp.theorangeone.net/js/index.js"></script>
|
||||
{{ end }}
|
||||
|
|
29
layouts/_default/search.html
Normal file
29
layouts/_default/search.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{{ define "main" }}
|
||||
{{ $pages := where .Site.RegularPages "Section" "!=" ""}}
|
||||
|
||||
<div id="main" class="search-page">
|
||||
<div class="container">
|
||||
{{ partial "content.html" . }}
|
||||
<div class="input-group mt-5">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text"><i class="fas fa-search"></i></div>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="search-input" placeholder="Search">
|
||||
</div>
|
||||
|
||||
<p id="search-results-count" class="mt-5 mb-4"></p>
|
||||
|
||||
<div class="search-results">
|
||||
{{ range $pages }}
|
||||
{{ partial "list_item.html" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<h5 id="search-results-message" class="text-center mt-5"></h5>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "scripts_extra" }}
|
||||
{{ partial "script_async.html" "js/search.js" }}
|
||||
{{ end }}
|
8
layouts/_default/search.json
Normal file
8
layouts/_default/search.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{- $result := slice -}}
|
||||
|
||||
{{- range (where .Site.RegularPages "Section" "!=" "") -}}
|
||||
{{- $data := dict "title" .Title "content" .Plain "id" (.File.UniqueID) -}}
|
||||
{{- $result = $result | append $data -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ jsonify $result }}
|
|
@ -5,6 +5,12 @@
|
|||
<div class="text-center m-auto text-light">
|
||||
<h1 class="display-1">{{ .Site.Params.author_name }}</h1>
|
||||
<h2 class="lead">{{ .Content }}</h2>
|
||||
|
||||
<form action='{{ (.Site.GetPage "search").RelPermalink }}' method="GET">
|
||||
<div class="input-group mt-5">
|
||||
<input name="q" type="text" class="form-control" id="search-input" placeholder="Search" title="Press enter to search">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
{{ end }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="media list-page-item mb-3">
|
||||
<div class="media list-page-item mb-3" data-id="{{ .File.UniqueID }}">
|
||||
{{ if .Params.image }}
|
||||
<div class="d-none d-md-block align-self-center img-wrapper mr-3">
|
||||
<a href="{{ .RelPermalink }}">
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
{{ $pages := where .Site.RegularPages.ByTitle "Section" "" }}
|
||||
{{ $nav_pages := union $pages .Site.Sections }}
|
||||
|
||||
{{ $nav_pages = where $nav_pages "Params.hidden_from_navbar" "!=" true }}
|
||||
|
||||
{{ range sort $nav_pages "LinkTitle" }}
|
||||
<li class="nav-item">
|
||||
<a href="{{ .RelPermalink }}" class="nav-link">
|
||||
|
|
|
@ -10,11 +10,10 @@
|
|||
|
||||
<div class="d-none d-lg-block">
|
||||
<ul class="navbar-nav">
|
||||
{{ range .Site.Data.social.navbar_accounts }}
|
||||
{{ $account := index $.Site.Data.social.accounts . }}
|
||||
{{ with (.Site.GetPage "search") }}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ $account.link }}">
|
||||
<i class="{{ $account.icon }}" title="{{ $account.name }}"></i>
|
||||
<a class="nav-link" href="{{ .Permalink }}">
|
||||
<i class="fas fa-search" title="Search"></i>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
@ -25,6 +24,14 @@
|
|||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ range .Site.Data.social.navbar_accounts }}
|
||||
{{ $account := index $.Site.Data.social.accounts . }}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ $account.link }}">
|
||||
<i class="{{ $account.icon }}" title="{{ $account.name }}"></i>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -13,7 +13,8 @@
|
|||
"elevator.js": "1.0.1",
|
||||
"jquery": "3.6.0",
|
||||
"lg-thumbnail": "1.2.1",
|
||||
"lightgallery": "1.10.0"
|
||||
"lightgallery": "1.10.0",
|
||||
"lunr": "2.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"broken-link-checker-local": "0.2.1",
|
||||
|
@ -4564,6 +4565,11 @@
|
|||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
|
||||
},
|
||||
"node_modules/map-age-cleaner": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||
|
@ -11480,6 +11486,11 @@
|
|||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="
|
||||
},
|
||||
"map-age-cleaner": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"elevator.js": "1.0.1",
|
||||
"jquery": "3.6.0",
|
||||
"lg-thumbnail": "1.2.1",
|
||||
"lightgallery": "1.10.0"
|
||||
"lightgallery": "1.10.0",
|
||||
"lunr": "2.3.9"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ rm -rf $OUTPUT_DIR
|
|||
rm -rf $STATIC_BUILD
|
||||
rm -rf $BASEDIR/resources
|
||||
|
||||
mkdir -p $STATIC_BUILD/js $STATIC_BUILD/css $STATIC_BUILD/audio
|
||||
mkdir -p $STATIC_BUILD/css $STATIC_BUILD/audio
|
||||
|
||||
cp -r $BASEDIR/node_modules/lightgallery/dist/fonts $STATIC_BUILD
|
||||
cp -r $STATIC_SRC/img $STATIC_BUILD/img
|
||||
|
@ -17,7 +17,7 @@ cp -r $BASEDIR/node_modules/@fortawesome/fontawesome-free/css/all.min.css $STATI
|
|||
cp $BASEDIR/node_modules/lightgallery/dist/css/lightgallery.min.css $STATIC_BUILD/css/lightgallery.css
|
||||
cp -r $BASEDIR/node_modules/@fortawesome/fontawesome-free/webfonts $STATIC_BUILD
|
||||
|
||||
cp $STATIC_SRC/js/app.js $STATIC_BUILD/js/app.js
|
||||
cp -r $STATIC_SRC/js/ $STATIC_BUILD/js/
|
||||
cp $BASEDIR/node_modules/jquery/dist/jquery.min.js $STATIC_BUILD/js/jquery.min.js
|
||||
cp $BASEDIR/node_modules/lightgallery/dist/js/lightgallery-all.min.js $STATIC_BUILD/js/lightgallery.min.js
|
||||
cp $BASEDIR/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js $STATIC_BUILD/js/bootstrap.min.js
|
||||
|
|
110
static/src/js/search.js
Normal file
110
static/src/js/search.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
const lunr = require('lunr');
|
||||
|
||||
const MIN_SEARCH_LENGTH = 3;
|
||||
|
||||
const searchInput = $('#search-input');
|
||||
const searchResultsMessage = $('#search-results-message');
|
||||
const searchResultsCount = $('#search-results-count');
|
||||
const searchResultsList = $('.search-results');
|
||||
const searchResults = searchResultsList.children();
|
||||
|
||||
let searchIndex;
|
||||
|
||||
function showMessage(message) {
|
||||
if (message) {
|
||||
searchResults.hide();
|
||||
searchResultsCount.hide();
|
||||
searchResultsMessage.show();
|
||||
searchResultsMessage.text(message);
|
||||
} else {
|
||||
searchResultsMessage.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialSearchTerm() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('q') || '';
|
||||
}
|
||||
|
||||
function onSearch(term) {
|
||||
if (term.length < MIN_SEARCH_LENGTH) {
|
||||
searchResultsCount.hide();
|
||||
showMessage(
|
||||
`Enter a search term (of at least ${MIN_SEARCH_LENGTH} characters) to search`
|
||||
);
|
||||
return;
|
||||
}
|
||||
showMessage('');
|
||||
|
||||
// do search
|
||||
const results = searchIndex.search(term).map(match => match.ref);
|
||||
|
||||
if (!results.length) {
|
||||
showMessage('No results found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the results
|
||||
searchResults.each(function() {
|
||||
const el = $(this);
|
||||
if (results.includes(this.dataset.id)) {
|
||||
el.css('display', 'flex');
|
||||
} else {
|
||||
el.css('display', 'none');
|
||||
}
|
||||
});
|
||||
|
||||
// Sort results by accuracy
|
||||
searchResults
|
||||
.sort(function(a, b) {
|
||||
const aOrder = results.indexOf(a.dataset.id);
|
||||
const bOrder = results.indexOf(b.dataset.id);
|
||||
if (aOrder < bOrder) {
|
||||
return -1;
|
||||
}
|
||||
if (aOrder > bOrder) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.appendTo(searchResultsList);
|
||||
|
||||
// Show number of results
|
||||
searchResultsCount.text(`Found ${results.length} results`);
|
||||
searchResultsCount.show();
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
searchInput.prop('disabled', true);
|
||||
const initialSearchTerm = getInitialSearchTerm();
|
||||
searchInput.val(initialSearchTerm);
|
||||
|
||||
$.getJSON('./index.json', function(data) {
|
||||
// populate the index
|
||||
searchIndex = lunr(function() {
|
||||
this.ref('id');
|
||||
this.field('title');
|
||||
this.field('content');
|
||||
|
||||
data.forEach(function(page) {
|
||||
this.add(page);
|
||||
}, this);
|
||||
});
|
||||
|
||||
// add event handler
|
||||
searchInput.on('input', function(event) {
|
||||
onSearch(event.target.value);
|
||||
});
|
||||
|
||||
// re-enable the input
|
||||
searchInput.prop('disabled', false);
|
||||
|
||||
// Do an initial search
|
||||
onSearch(searchInput.val());
|
||||
|
||||
// Scroll to the title if we landed on the page with a search term
|
||||
if (initialSearchTerm) {
|
||||
document.getElementsByTagName('h1')[0].scrollIntoView();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -110,6 +110,17 @@ table td {
|
|||
#index-header {
|
||||
width: 100%;
|
||||
height: calc(100vh - #{$footer-height} - #{$navbar-height});
|
||||
|
||||
#search-input {
|
||||
border: 3px solid $black;
|
||||
background-color: opacify($black, 0.8);
|
||||
text-align: center;
|
||||
color: $body-color;
|
||||
|
||||
&::placeholder {
|
||||
color: opacify($body-color, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -335,3 +346,23 @@ a.no-color-change {
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#main.search-page {
|
||||
.search-results {
|
||||
.list-page-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#search-input {
|
||||
border: 0;
|
||||
background-color: opacify($black, 0.6);
|
||||
color: $body-color;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
border: 0;
|
||||
background-color: $black;
|
||||
color: $body-color;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue