This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-website.git


The following commit(s) were added to refs/heads/main by this push:
     new dab04254 fix: Remove duplicate search results for same parent page 
(#1464) (#1466)
dab04254 is described below

commit dab04254a011150305c9c45c5234ff3edae3c41b
Author: Ganesh Patil <[email protected]>
AuthorDate: Tue Jan 6 17:51:10 2026 +0530

    fix: Remove duplicate search results for same parent page (#1464) (#1466)
    
    * fix(search): limit search result snippets to 200 chars and increase 
result cap
    
    Truncates each search result description to 200 characters
    
    Prevents result list overflow that hides other hits
    
    Updated search result return limit from 5 to 10
    
    Added CSS line-clamp for cleaner result display
    
    * fix(search): prioritize core docs and limit input length (#1459)
    
    - Add 200 character max length to search input (HTML + JS validation)
    - Prioritize core documentation (manual, user-guide, architecture) over 
component pages
    - Add CSS constraints (max-width, min-width) to prevent dropdown UI overflow
    - Fetch more results (20) from Algolia then filter and sort to top 10
    - Core docs patterns: /manual/, /user-guide/, /architecture/, 
/getting-started/, /faq/
    - Component pages now rank lower in search results
    
    * fix: remove duplicate search results for same parent page (#1464)
---
 antora-ui-camel/src/css/header.css                 |   2 +
 .../src/js/vendor/algoliasearch.bundle.js          | 149 ++++++++++++++++++++-
 antora-ui-camel/src/partials/header-content.hbs    |   2 +-
 3 files changed, 148 insertions(+), 5 deletions(-)

diff --git a/antora-ui-camel/src/css/header.css 
b/antora-ui-camel/src/css/header.css
index 8b265ec8..9eccac2b 100644
--- a/antora-ui-camel/src/css/header.css
+++ b/antora-ui-camel/src/css/header.css
@@ -303,6 +303,8 @@ html:not([data-scroll='0']) .navbar {
   margin-right: 10px;
   overflow-y: auto;
   max-height: 80vh;
+  max-width: min(600px, 90vw);
+  min-width: 300px;
   scrollbar-width: thin; /* Firefox */
 }
 
diff --git a/antora-ui-camel/src/js/vendor/algoliasearch.bundle.js 
b/antora-ui-camel/src/js/vendor/algoliasearch.bundle.js
index a4b692ab..5951d366 100644
--- a/antora-ui-camel/src/js/vendor/algoliasearch.bundle.js
+++ b/antora-ui-camel/src/js/vendor/algoliasearch.bundle.js
@@ -5,6 +5,7 @@
 
   const MAX_SNIPPET_LENGTH = 200
   const RESULTS_LIMIT = 10
+  const MAX_INPUT_LENGTH = 200
 
   // Sub-projects to exclude from main search - users can browse these directly
   const EXCLUDED_SUBPROJECTS = [
@@ -16,6 +17,15 @@
     '/camel-karaf/',
   ]
 
+  // Core docs patterns - these should rank higher than component pages
+  const CORE_DOCS_PATTERNS = [
+    '/manual/',
+    '/user-guide/',
+    '/architecture/',
+    '/getting-started/',
+    '/faq/',
+  ]
+
   // Check if a URL belongs to a sub-project that should be filtered out
   function isSubProjectUrl (url) {
     if (!url) return false
@@ -24,6 +34,125 @@
     })
   }
 
+  // Check if a URL points to core documentation (should rank higher)
+  function isCoreDocsUrl (url) {
+    if (!url) return false
+    return CORE_DOCS_PATTERNS.some(function (pattern) {
+      return url.indexOf(pattern) !== -1
+    })
+  }
+
+  // Check if a URL points to component documentation
+  function isComponentUrl (url) {
+    if (!url) return false
+    return url.indexOf('/components/') !== -1
+  }
+
+  // Sort hits to prioritize core docs over components
+  function sortByCoreDocs (hits) {
+    return hits.sort(function (a, b) {
+      var aIsCore = isCoreDocsUrl(a.url)
+      var bIsCore = isCoreDocsUrl(b.url)
+      var aIsComponent = isComponentUrl(a.url)
+      var bIsComponent = isComponentUrl(b.url)
+
+      // Core docs first
+      if (aIsCore && !bIsCore) return -1
+      if (!aIsCore && bIsCore) return 1
+
+      // Components last
+      if (aIsComponent && !bIsComponent) return 1
+      if (!aIsComponent && bIsComponent) return -1
+
+      return 0
+    })
+  }
+
+  // Extract the parent page path from a URL (removes anchor and trailing 
segments)
+  function getParentPagePath (url) {
+    if (!url) return ''
+    // Remove anchor fragment
+    var path = url.split('#')[0]
+    // Normalize trailing slash
+    if (path.endsWith('/')) {
+      path = path.slice(0, -1)
+    }
+    return path
+  }
+
+  // Check if hit represents a sub-section of a page (has anchor or deeper 
hierarchy)
+  function isSubSection (hit) {
+    if (!hit || !hit.url) return false
+    return hit.url.indexOf('#') !== -1
+  }
+
+  // Get the breadcrumb depth (number of hierarchy levels)
+  function getBreadcrumbDepth (hit) {
+    if (!hit || !hit.hierarchy) return 0
+    return Object.values(hit.hierarchy).filter(function (lvl) {
+      return lvl !== null
+    }).length
+  }
+
+  // Remove duplicate results for the same parent page
+  // When parent page is a direct match, exclude its sub-sections
+  function deduplicateHits (hits, query) {
+    var seenPages = {}
+    var parentMatches = {}
+    var queryLower = (query || '').toLowerCase().trim()
+
+    // First pass: identify parent pages that match the query directly
+    hits.forEach(function (hit) {
+      var parentPath = getParentPagePath(hit.url)
+      var hierarchy = hit.hierarchy || {}
+
+      // Check if any top-level hierarchy matches the search query
+      var lvl1 = (hierarchy.lvl1 || '').toLowerCase()
+      var lvl0 = (hierarchy.lvl0 || '').toLowerCase()
+
+      if (lvl1 && lvl1.indexOf(queryLower) !== -1) {
+        parentMatches[parentPath] = true
+      }
+      if (lvl0 && lvl0.indexOf(queryLower) !== -1) {
+        parentMatches[parentPath] = true
+      }
+    })
+
+    // Second pass: filter out sub-sections when parent is already matched
+    return hits.filter(function (hit) {
+      var parentPath = getParentPagePath(hit.url)
+      var isSubSec = isSubSection(hit)
+      var depth = getBreadcrumbDepth(hit)
+
+      // If this is a sub-section and parent page already matched, skip it
+      if (isSubSec && parentMatches[parentPath]) {
+        // Only keep the main page hit, not sub-sections
+        if (seenPages[parentPath]) {
+          return false
+        }
+      }
+
+      // For component pages, only show the main entry (depth <= 2)
+      if (isComponentUrl(hit.url) && depth > 2 && seenPages[parentPath]) {
+        return false
+      }
+
+      // Track that we've seen this parent page
+      if (!seenPages[parentPath]) {
+        seenPages[parentPath] = { depth: depth, hit: hit }
+        return true
+      }
+
+      // If we already have this page, only keep if it's a better match 
(shallower)
+      if (depth < seenPages[parentPath].depth) {
+        seenPages[parentPath] = { depth: depth, hit: hit }
+        return true
+      }
+
+      return false
+    })
+  }
+
   function truncateHighlightedHtml (html, maxChars) {
     if (!html || maxChars <= 0) return ''
 
@@ -104,6 +233,13 @@
       e.stopPropagation()
     })
 
+    // Enforce max input length as backup to HTML maxlength attribute
+    search.addEventListener('input', function () {
+      if (search.value.length > MAX_INPUT_LENGTH) {
+        search.value = search.value.substring(0, MAX_INPUT_LENGTH)
+      }
+    })
+
     search.addEventListener(
       'keyup',
       debounce((key) => {
@@ -119,16 +255,21 @@
           return
         }
         cancel.style.display = 'block'
+        var searchQuery = search.value
         index
-          .search(search.value, {
-            hitsPerPage: 10,
+          .search(searchQuery, {
+            hitsPerPage: 50,
           })
           .then((results) => {
             // Filter out sub-project results to focus on camel-core 
documentation
             const filteredHits = results.hits.filter(function (hit) {
               return !isSubProjectUrl(hit.url)
-            }).slice(0, RESULTS_LIMIT)
-            const data = filteredHits.reduce((data, hit) => {
+            })
+            // Remove duplicate results for the same parent page
+            const dedupedHits = deduplicateHits(filteredHits, searchQuery)
+            // Sort to prioritize core docs over components and limit results
+            const sortedHits = sortByCoreDocs(dedupedHits).slice(0, 
RESULTS_LIMIT)
+            const data = sortedHits.reduce((data, hit) => {
               const section = hit.hierarchy.lvl0
               const sectionKey = `${section}-${hit.version || ''}`
 
diff --git a/antora-ui-camel/src/partials/header-content.hbs 
b/antora-ui-camel/src/partials/header-content.hbs
index 3173cfc9..4ae105d5 100644
--- a/antora-ui-camel/src/partials/header-content.hbs
+++ b/antora-ui-camel/src/partials/header-content.hbs
@@ -17,7 +17,7 @@
       <div class="navbar-fill"></div>
       <div class="break-row"></div>
       <div class="navbar-search results-hidden">
-        <input id="search" class="search" placeholder="Search" 
autocomplete="off">
+        <input id="search" class="search" placeholder="Search" 
autocomplete="off" maxlength="200">
         <img src="{{uiRootPath}}/img/cancel.svg" alt="Clear" 
id="search-cancel">
         <div id="search_results"></div>
       </div>

Reply via email to