Fixing Search Problems in Hugo Hextra theme

Fixing Search Problems in Hugo Hextra theme

July 6, 2025

The Hugo Hextra theme used by this blog has a useful search feature, implemented using the popular FlexSearch library. But after playing with search for a while, I discovered that it often returned only a small subset of the expected posts. For example, my blog has 20 posts that contain the word “firefox”, but search displayed only five of those posts. Here I describe how to fix this problem, which actually turned out to be several problems.

Hardcoded Limits

The code that implements search is the file themes/hextra/assets/js/flexsearch.js.
After examining that file, I could see that the limit on the number of posts returned by the page search was a hardcoded 5:

    const pageResults = window.pageIndex.search(query, 5,
      { enrich: true, suggest: true })[0]?.result || [];

Further down, the number of sections (i.e., paragraphs) returned for each found page was also a hardcoded 5:

      const sectionResults = window.sectionIndex.search(query, 5,
        { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];

The Hextra author kindly provided a patch that allowed these two limits to be configurable in hugo.yaml. Here are the revised versions of the two code snippets above:

    // Configurable search limits with sensible defaults
    const maxPageResults = parseInt('{{- site.Params.search.flexsearch.maxPageResults |
       default 20 -}}', 10);
    const maxSectionResults = parseInt('{{- site.Params.search.flexsearch.maxSectionResults |
       default 10 -}}', 10);

    const pageResults = window.pageIndex.search(query, maxPageResults,
      { enrich: true, suggest: true })[0]?.result || [];
...
      const sectionResults = window.sectionIndex.search(query, maxSectionResults,
        { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];

Here are the relevant lines from hugo.yaml:

params:
...
  search:
    enable: true
    type: flexsearch

    flexsearch:
      index: content
      maxPageResults: 50
      maxSectionResults: 3

FlexSearch Bug?

But the problem persisted. In particular, when I set maxSectionResults to a small number, say 3, it seemed to restrict the total number of found pages (i.e., posts) to 3, as if maxPageResults had no effect. I couldn’t see any more problems with the Hextra code, so I started looking at FlexSearch issues to see if anybody had reported this issue. I did find one issue that seemed similar. It appeared to me that the search of sectionIndex was misbehaving if a limit parameter was supplied. So I removed the limit parameter to the search, and instead used maxSectionResults to limit the subsequent loop through the results:

      const sectionResults = window.sectionIndex.search(query,
        { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];
...
      const nResults = Math.min(sectionResults.length, maxSectionResults);
      for (let j = 0; j < nResults; j++) {
...

With this change, search finally started returning the expected number of posts and sections (i.e., paragraphs) within those posts.

Issue and Diff

I reported these problems in this issue.

Here is the complete diff for my changes to flexsearch.js:

--- themes/hextra/assets/js/flexsearch.js	2025-07-05 11:28:42.527276630 -0700
+++ assets/js/flexsearch.js	2025-07-06 19:17:14.088721440 -0700
@@ -244,7 +244,7 @@
 
         const crumbData = data[searchUrl];
         if (!crumbData) {
-          console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route);
+          // console.warn('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route);
           continue;
         }
 
@@ -318,7 +318,11 @@
     }
     resultsElement.classList.remove('hx:hidden');
 
-    const pageResults = window.pageIndex.search(query, 5, { enrich: true, suggest: true })[0]?.result || [];
+    // Configurable search limits with sensible defaults
+    const maxPageResults = parseInt('{{- site.Params.search.flexsearch.maxPageResults | default 20 -}}', 10);
+    const maxSectionResults = parseInt('{{- site.Params.search.flexsearch.maxSectionResults | default 10 -}}', 10);
+
+    const pageResults = window.pageIndex.search(query, maxPageResults, { enrich: true, suggest: true })[0]?.result || [];
 
     const results = [];
     const pageTitleMatches = {};
@@ -327,12 +331,13 @@
       const result = pageResults[i];
       pageTitleMatches[i] = 0;
 
-      // Show the top 5 results for each page
-      const sectionResults = window.sectionIndex.search(query, 5, { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];
+      const sectionResults = window.sectionIndex.search(query,
+        { enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];
       let isFirstItemOfPage = true
       const occurred = {}
 
-      for (let j = 0; j < sectionResults.length; j++) {
+      const nResults = Math.min(sectionResults.length, maxSectionResults);
+      for (let j = 0; j < nResults; j++) {
         const { doc } = sectionResults[j]
         const isMatchingTitle = doc.display !== undefined
         if (isMatchingTitle) {