Browse Source

search: upgrade to Fuse.js 7.5.0 and load the index from a JSON file

- the search index is generated by Hugo as a separate JSON file
  (searchindex output format), loaded by the search page and cached by
  the browser: the search pages go from 96 KB to 7 KB
- Fuse.js 6.4.3 -> 7.5.0 (basic build). Since Fuse.js 7 no longer
  provides an UMD build, search.js is now an ES module importing it.
- search on the input event instead of keyup (paste, autocompletion)

Refs #11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNHsWYTXPThURz6TbVS1j3
master
Nicolas Massé 1 month ago
parent
commit
e67dbba625
  1. 8
      config.yaml
  2. 4
      content/english/search/index.md
  3. 4
      content/french/search/index.md
  4. 9
      layouts/_partials/head.html
  5. 41
      layouts/search.html
  6. 34
      layouts/search.searchindex.json
  7. 9
      static/js/fuse.basic.min.js
  8. 9
      static/js/fuse.js
  9. 90
      static/js/search.js

8
config.yaml

@ -102,6 +102,14 @@ privacy:
youtube: youtube:
privacyEnhanced: true privacyEnhanced: true
# The search index, used by the search page (static/js/search.js)
outputFormats:
searchindex:
mediaType: application/json
baseName: index
isPlainText: true
notAlternative: true
taxonomies: taxonomies:
opensource: "opensource" opensource: "opensource"
topic: "topics" topic: "topics"

4
content/english/search/index.md

@ -2,4 +2,8 @@
title: "Search" title: "Search"
type: search type: search
layout: search layout: search
# The search index is generated as a separate JSON file (see config.yaml)
outputs:
- html
- searchindex
--- ---

4
content/french/search/index.md

@ -2,4 +2,8 @@
title: "Recherche" title: "Recherche"
type: search type: search
layout: search layout: search
# The search index is generated as a separate JSON file (see config.yaml)
outputs:
- html
- searchindex
--- ---

9
layouts/_partials/head.html

@ -59,15 +59,6 @@
{{ partial "opengraph.html" . }} {{ partial "opengraph.html" . }}
{{ partial "twitter_cards.html" . }} {{ partial "twitter_cards.html" . }}
<!-- The search page is the only one to require Javascript -->
{{ if eq .Type "search" }}
<!--
-- Download and copy over fuse.js file from fusejs.io.
-- I used the UMD build, minified, version 6.4.3
-->
<script src="/js/fuse.js"></script>
{{ end }}
{{- if .Site.Params.enablePlausible -}} {{- if .Site.Params.enablePlausible -}}
<script defer data-domain="itix.fr" src="https://plausible.io/js/plausible.js"></script> <script defer data-domain="itix.fr" src="https://plausible.io/js/plausible.js"></script>
{{- end -}} {{- end -}}

41
layouts/search.html

@ -19,40 +19,13 @@
{{- end }} {{- end }}
</div> </div>
{{- /* {{- /*
-- The search data are embedded in a JSON data block, used by /js/search.js -- The search index is a separate JSON file (search.searchindex.json),
-- loaded by /js/search.js. Only the URL of the index and the translated
-- strings are embedded in the page, in a JSON data block (no inline
-- script, see the Content Security Policy).
*/ -}} */ -}}
{{- /* Taxonomies */ -}} {{- $config := dict "indexUrl" (.OutputFormats.Get "searchindex").RelPermalink "i18n" (dict "published_on" (i18n "published_on") "continue_reading" (i18n "continue_reading")) }}
{{- $.Store.Set "taxonomies" dict -}} <script type="application/json" id="search-config">{{ $config | jsonify | safeJS }}</script>
{{- range $name, $taxonomy := .Site.Taxonomies -}} <script type="module" src="/js/search.js"></script>
{{- $.Store.Set "taxonomy" dict -}}
{{- range (index $.Site.Taxonomies $name) -}}
{{- $.Store.SetInMap "taxonomy" .Page.Title (dict "href" .Page.RelPermalink "title" .Page.Title) -}}
{{- end -}}
{{- $.Store.SetInMap "taxonomies" $name ($.Store.Get "taxonomy") -}}
{{- end }}
{{- /* Search index */ -}}
{{- $.Store.Set "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Store.Set "reading_time" nil -}}
{{- if eq .Type "blog" -}}
{{- $minutes := .ReadingTime -}}
{{- if gt $minutes 0 -}}
{{- $.Store.Set "reading_time" (i18n "minutes" $minutes) -}}
{{- end -}}
{{- end -}}
{{- $.Store.Set "date" nil -}}
{{- if ne .Type "page" -}}
{{- $.Store.Set "date" (.Date.Format (i18n "date_format")) -}}
{{- end -}}
{{ .Store.Set "en_article_in_fr_site" false }}
{{- partial "lang-detection.html" . -}}
{{- $.Store.Add "index" (dict "title" .Title "opensource" .Params.opensource "topics" .Params.topics "permalink" .Permalink "hasEnFlag" (.Store.Get "en_article_in_fr_site") "type" .Type "type_label" (i18n (print "type_" .Type)) "date" ($.Store.Get "date") "reading_time" ($.Store.Get "reading_time") "summary" (.Summary | plainify | htmlUnescape)) -}}
{{- end }}
{{- $data := dict "taxonomies" ($.Store.Get "taxonomies") "index" ($.Store.Get "index") "i18n" (dict "published_on" (i18n "published_on") "continue_reading" (i18n "continue_reading")) }}
<script type="application/json" id="search-data">{{ $data | jsonify | safeJS }}</script>
<script src="/js/search.js"></script>
</article> </article>
{{ end }} {{ end }}

34
layouts/search.searchindex.json

@ -0,0 +1,34 @@
{{- /*
-- The search index of the "search" page, loaded by /js/search.js.
*/ -}}
{{- /* Taxonomies */ -}}
{{- $.Store.Set "taxonomies" dict -}}
{{- range $name, $taxonomy := .Site.Taxonomies -}}
{{- $.Store.Set "taxonomy" dict -}}
{{- range (index $.Site.Taxonomies $name) -}}
{{- $.Store.SetInMap "taxonomy" .Page.Title (dict "href" .Page.RelPermalink "title" .Page.Title) -}}
{{- end -}}
{{- $.Store.SetInMap "taxonomies" $name ($.Store.Get "taxonomy") -}}
{{- end -}}
{{- /* Search index */ -}}
{{- $.Store.Set "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Store.Set "reading_time" nil -}}
{{- if eq .Type "blog" -}}
{{- $minutes := .ReadingTime -}}
{{- if gt $minutes 0 -}}
{{- $.Store.Set "reading_time" (i18n "minutes" $minutes) -}}
{{- end -}}
{{- end -}}
{{- $.Store.Set "date" nil -}}
{{- if ne .Type "page" -}}
{{- $.Store.Set "date" (.Date.Format (i18n "date_format")) -}}
{{- end -}}
{{- .Store.Set "en_article_in_fr_site" false -}}
{{- partial "lang-detection.html" . -}}
{{- $.Store.Add "index" (dict "title" .Title "opensource" .Params.opensource "topics" .Params.topics "permalink" .Permalink "hasEnFlag" (.Store.Get "en_article_in_fr_site") "type" .Type "type_label" (i18n (print "type_" .Type)) "date" ($.Store.Get "date") "reading_time" ($.Store.Get "reading_time") "summary" (.Summary | plainify | htmlUnescape)) -}}
{{- end -}}
{{- dict "taxonomies" ($.Store.Get "taxonomies") "index" ($.Store.Get "index") | jsonify -}}

9
static/js/fuse.basic.min.js

File diff suppressed because one or more lines are too long

9
static/js/fuse.js

File diff suppressed because one or more lines are too long

90
static/js/search.js

@ -1,13 +1,17 @@
// //
// Search engine of the "search" page. // Search engine of the "search" page (ES module).
// //
// The search index, the taxonomies and the translated strings are generated // The search index and the taxonomies are generated by Hugo as a separate
// by Hugo in a JSON data block (see layouts/search.html), so that no inline // JSON file (see layouts/search.searchindex.json), loaded when the page is
// displayed and cached by the browser. The URL of the index and the translated
// strings are embedded in the page in a JSON data block, so that no inline
// script is needed (Content Security Policy). // script is needed (Content Security Policy).
// //
var searchData = JSON.parse(document.getElementById('search-data').textContent); import Fuse from './fuse.basic.min.js';
var taxonomies = searchData.taxonomies;
var searchDb = searchData.index; var searchConfig = JSON.parse(document.getElementById('search-config').textContent);
var taxonomies = {};
var fuse = null;
var searchResults = document.getElementById('searchResults'); // targets the <div> above var searchResults = document.getElementById('searchResults'); // targets the <div> above
var searchInput = document.getElementById('searchInput'); // input box for search var searchInput = document.getElementById('searchInput'); // input box for search
@ -50,7 +54,7 @@ function printSearchResult(result) {
if (result.item.type != "page") { if (result.item.type != "page") {
var div = tag("div", { 'class': 'post-metadata'}); var div = tag("div", { 'class': 'post-metadata'});
var span_date = tag("span", { 'class': 'publication-date' }); var span_date = tag("span", { 'class': 'publication-date' });
span_date.textContent = searchData.i18n.published_on + " " + result.item.date; span_date.textContent = searchConfig.i18n.published_on + " " + result.item.date;
div.appendChild(span_date); div.appendChild(span_date);
if (result.item.topics != null) { if (result.item.topics != null) {
result.item.topics.forEach(function (v) { result.item.topics.forEach(function (v) {
@ -105,7 +109,7 @@ function printSearchResult(result) {
var p = tag("p", {}); var p = tag("p", {});
p.textContent = result.item.summary; p.textContent = result.item.summary;
var a = tag("a", { href: result.item.permalink, 'class': 'continue-reading' }); var a = tag("a", { href: result.item.permalink, 'class': 'continue-reading' });
a.textContent = searchData.i18n.continue_reading; a.textContent = searchConfig.i18n.continue_reading;
p.appendChild(a); p.appendChild(a);
section.appendChild(p); section.appendChild(p);
@ -121,28 +125,40 @@ function clearSearchResults() {
} }
// //
// load the search index // Fuse.js options; check fuse.js website for details
// //
function fuseInit(searchDb) { var fuseOptions = {
var options = { // fuse.js options; check fuse.js website for details shouldSort: true,
shouldSort: true, ignoreLocation: true,
ignoreLocation: true, threshold: 0.4,
threshold: 0.4, minMatchCharLength: 3,
minMatchCharLength: 3, keys: [
keys: [ 'title',
'title', 'permalink',
'permalink', 'summary',
'summary', 'topics',
'topics', 'opensource'
'opensource' ]
] };
};
fuse = new Fuse(searchDb, options); // build the index from the json file //
} // Load the search index and initialize Fuse.js
//
var searchReady = fetch(searchConfig.indexUrl)
.then(function (response) {
if (!response.ok) {
throw new Error("Cannot load the search index: HTTP " + response.status);
}
return response.json();
})
.then(function (data) {
taxonomies = data.taxonomies;
fuse = new Fuse(data.index, fuseOptions);
});
// //
// using the index loaded previously, run a search query (for "term") // Run a search query (for "term") every time a letter is typed in the
// every time a letter is typed in the search box // search box
// //
function executeSearch(term) { function executeSearch(term) {
if (lastSearch == term) { if (lastSearch == term) {
@ -156,12 +172,12 @@ function executeSearch(term) {
lastSearch = term; lastSearch = term;
} }
// Initialize the Fuse.io framework with the Search DB searchInput.addEventListener('input', function () {
fuseInit(searchDb); var term = this.value;
searchReady.then(function () {
// // Only display the results of the latest search terms
// execute search as each character is typed if (searchInput.value === term) {
// executeSearch(term);
searchInput.onkeyup = function(e) { }
executeSearch(this.value); });
} });

Loading…
Cancel
Save