From 5b87aa3a50b3c4365073502758ad6628d19b4322 Mon Sep 17 00:00:00 2001 From: ppipada Date: Mon, 27 May 2024 19:05:27 +0530 Subject: [PATCH] rebuilding site Mon May 27 07:05:27 PM IST 2024 --- index.json | 2 +- index.xml | 2 +- ...5b9b4eed6c8b672abb5b753bdca314ba6b6f8b07fc5768.js | 11 +++++++++++ posts/2024-05-27-hugo-search/index.html | 12 +++++++++--- posts/index.xml | 2 +- search/index.html | 2 +- sitemap.xml | 10 +++++----- tags/hugo/index.xml | 2 +- tags/index.xml | 2 +- 9 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 js/search.min.7e968a9ef35980108f5b9b4eed6c8b672abb5b753bdca314ba6b6f8b07fc5768.js diff --git a/index.json b/index.json index 2eaba79..3dcb4aa 100644 --- a/index.json +++ b/index.json @@ -9,7 +9,7 @@ "url": "https://pankajpipada.com/posts/2024-05-27-hugo-datatables/" }, { - "content": "Adding search functionality to a static site generated with Hugo can significantly improve the user experience. lunr.js is a powerful JavaScript library for full-text search, offering a lightweight and fast solution ideal for static sites. In this guide, we will integrate search using lunr.js and address optimization concerns to ensure efficient and smooth operation. Note that the examples use Bootstrap and Font Awesome icons, but the same elements can be adapted to any styling system as required.\nStep 1: Create a data index for search In your config.toml, add the following lines to enable the generation of the search index file: [outputs] home = [\"HTML\", \"RSS\", \"JSON\"] [outputFormats] [outputFormats.JSON] mediaType = \"application/json\" baseName = \"index\" isPlainText = true Then, create a layouts/index.json file, that will have a template for creating the data index. This file will be processed during hugo build to create a {output dir e.g public}/index.json For example, if you want to have title, url, content, tags, date available, the template will look something like below: {{- $index := slice -}} {{- range $.Site.RegularPages -}} {{- $tags := slice }} {{- range .Params.tags -}} {{- $tags = $tags | append . }} {{- end -}} {{- $content := .Content | plainify | htmlUnescape }} {{- $datestr := .Date.Format \"Jan 2, 2006\" }} {{- $indexItem := dict \"url\" .Permalink \"title\" .Title \"content\" $content \"tags\" $tags \"date\" $datestr -}} {{- $index = $index | append $indexItem }} {{- end -}} {{- $index | jsonify (dict \"indent\" \" \") }} Step 2: Create the Search Form Now that we have the data created for search, we would need to establish a interaction mechanism with the user. You can embed the search form in the header or body of all pages or restrict it to a dedicated search page. It can be embedded as {{ partial \"search-form.html\" . }} This form should, take input from user, and on action, invoke the search page with search query embedded into the url. Below is a slightly opinionated layouts/partials/search-form.html, using Bootstrap classes and Font Awesome icons for styling. This can be adapted to any styling system. The form takes user input and invokes the search page with the search query embedded into the URL. \u003c!-- Form with a get page action --\u003e \u003cform id=\"search\" action='{{ with .GetPage \"/search\" }}{{.Permalink}}{{end}}' method=\"get\" class=\"d-flex justify-content-center mt-2 mb-4\"\u003e \u003c!-- Hidden label for accessibility --\u003e \u003clabel hidden for=\"search-input\"\u003eSearch site\u003c/label\u003e \u003cdiv class=\"input-group\" style=\"width: 90%;\"\u003e \u003c!-- Icon inside the input group for visual enhancement --\u003e \u003cspan class=\"input-group-text border-0 bg-transparent\"\u003e \u003ci class=\"fa fa-search\"\u003e\u003c/i\u003e \u003c/span\u003e \u003c!-- Search input field. The \"name\" defined here will be used to parse the URL when executing the business logic in search.js --\u003e \u003cinput type=\"text\" class=\"form-control rounded-pill\" id=\"search-input\" name=\"query\" placeholder=\"Type here to search...\" aria-label=\"Search\"\u003e \u003c!-- Submit button with an arrow icon --\u003e \u003cbutton class=\"btn border-0 bg-transparent\" type=\"submit\" aria-label=\"search\"\u003e \u003ci class=\"fa fa-arrow-right\"\u003e\u003c/i\u003e \u003c/button\u003e \u003c/div\u003e \u003c/form\u003e Step 3: Set Up Your Search Content Page To actually execute the query and display the results we need to create a search content page. Generally it would be content/search/_index.md, but this can change depending on your chosen site organization for Hugo. This file should point to a search layout, that we will create below. You can customize it as required. Typically a bare minimum file will look like: --- title: \"Search\" layout: \"search\" description: \"Search page\" --- Step 4: Create the Search Layout Now, the above page needs to be served using a layout. The search layout defines the structure of the search page and includes necessary scripts for lunr.js and the custom search logic. By including these scripts in the layout page only, we can ensure that the search functionality is loaded on this page only and doesn’t really affect other pages. This can help optimize performance for the rest of the site. Create a search.html file in your layouts/_default directory: {{ define \"main\" }} \u003cdiv id=\"search-container\" class=\"container\"\u003e \u003c!-- This is where the search results will be displayed. Initialize as empty list. --\u003e \u003cul id=\"searchresults\"\u003e\u003c/ul\u003e \u003c/div\u003e \u003c!-- Include lunr.js library. This can be included from node_modules mounted as assets/vendor, refer to their cdn or directly use from static/js. --\u003e {{ $lunrJS := resources.Get \"vendor/lunr/lunr.min.js\" }} \u003cscript src=\"{{ $lunrJS.RelPermalink }}\" defer\u003e\u003c/script\u003e \u003c!-- Include the custom search script where the magic happens. This can be used from assets/js like below, or directly from static/js. --\u003e {{ with resources.Get \"js/search.js\" }} {{ $minifiedScript := . | minify | fingerprint }} \u003cscript src=\"{{ $minifiedScript.Permalink }}\" integrity=\"{{ $minifiedScript.Data.Integrity }}\" defer\u003e\u003c/script\u003e {{ else }} {{ errorf \"search.js not found in assets/js/\" }} {{ end }} {{ end }} Step 5: Create the Search business logic script Now we need to connect all the above site elements to lunr.js, perform search and render results. We will create a javascript script for this. This script handles the entire search process, including loading the search index, processing search queries, and displaying results. The below script can be placed as assets/js/search.js and included in your search layout as shown in a previous step. Alternately, you can put it directly inside static/js folder too and include it via the search layout above. Flow of the Code Initialization: The script initializes the lunr.js search index and ensures it only happens once for a page load. Caching: It checks for cached search data in localStorage. If valid cached data is available, it uses it; otherwise, it fetches new data and caches it. Building the Index: The script constructs the lunr.js search index from the fetched data. Search Query Handling: It reads the search query from the URL parameters and triggers a search if a query is present. Search Execution: It performs the search using the built index and processes the query to ensure it is valid. Displaying Results: It limits the displayed results to a maximum of 10 to avoid overwhelming users and improve performance. // Get the search input element var searchElem = document.getElementById(\"search-input\"); // Define a global object to store search-related data and ensure it's initialized only once window.pankajpipadaCom = window.pankajpipadaCom || {}; // Initialize search only once if (!window.pankajpipadaCom.initialized) { window.pankajpipadaCom.lunrIndex = null; window.pankajpipadaCom.posts = null; window.pankajpipadaCom.initialized = true; // Load search data and initialize lunr.js loadSearch(); } // Function to load search data and initialize lunr.js function loadSearch() { var now = new Date().getTime(); // Check for cached data in localStorage var storedData = localStorage.getItem(\"postData\"); // Use cached data if available and not expired if (storedData) { storedData = JSON.parse(storedData); if (now \u003c storedData.expiry) { console.log(\"Using cached data\"); buildIndex(storedData.data, checkURLAndSearch); return; } else { console.log(\"Cached data expired\"); localStorage.removeItem(\"postData\"); } } // Fetch search data via AJAX request var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState === 4 \u0026\u0026 xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); buildIndex(data, checkURLAndSearch); console.log(\"Search initialized\"); // Cache fetched data with expiry localStorage.setItem( \"postData\", JSON.stringify({ data: data, expiry: new Date().getTime() + 7 * 24 * 60 * 60 * 1000, // TTL for 1 week }) ); } catch (error) { console.error(\"Error parsing JSON:\", error); showError(\"Failed to load search data.\"); } } else if (xhr.status !== 200) { console.error(\"Failed to load data:\", xhr.status, xhr.statusText); showError(\"Failed to load search data.\"); } }; xhr.onerror = function () { console.error(\"Network error occurred.\"); showError(\"Failed to load search data due to network error.\"); }; xhr.open(\"GET\", \"../index.json\"); xhr.send(); } // Function to build lunr.js index function buildIndex(data, callback) { window.pankajpipadaCom.posts = data; window.pankajpipadaCom.lunrIndex = lunr(function () { this.ref(\"url\"); this.field(\"content\", { boost: 10 }); this.field(\"title\", { boost: 15 }); this.field(\"tags\"); this.field(\"date\"); window.pankajpipadaCom.posts.forEach(function (doc) { this.add(doc); }, this); }); console.log(\"Index built at\", new Date().toISOString()); callback(); } // Function to display error message function showError(message) { var searchResults = document.getElementById(\"searchresults\"); searchResults.innerHTML = `\u003cbr\u003e\u003ch2 style=\"text-align:center\"\u003e${message}\u003c/h2\u003e`; searchElem.disabled = true; // Disable search input on error } // Function to check URL for search query and perform search function checkURLAndSearch() { var urlParams = new URLSearchParams(window.location.search); var query = urlParams.get(\"query\"); if (query) { searchElem.value = query; showSearchResults(); } } // Function to perform search and display results function showSearchResults() { if (!window.pankajpipadaCom.lunrIndex) { console.log(\"Index not available.\"); return; // Exit function if index not loaded } var query = searchElem.value || \"\"; var searchString = query.trim().replace(/[^\\w\\s]/gi, \"\"); if (!searchString) { displayResults([]); return; // Exit if the search string is empty or only whitespace } var matches = window.pankajpipadaCom.lunrIndex.search(searchString); console.log(\"matches\", matches); var matchPosts = matches.map((m) =\u003e window.pankajpipadaCom.posts.find((p) =\u003e p.url === m.ref) ); console.log(\"Match posts\", matchPosts); displayResults(matchPosts); } // Function to display search results function displayResults(results) { const searchResults = document.getElementById(\"searchresults\"); const maxResults = 10; // Limit to 10 results if (results.length) { let resultList = \"\"; results.slice(0, maxResults).forEach((result) =\u003e { if (result) { resultList += getResultStr(result); } }); searchResults.innerHTML = resultList; } else { searchResults.innerHTML = \"No results found.\"; } } // Function to format search result items function getResultStr(result) { var resultList = ` \u003cli style=\"margin-bottom: 1rem\"\u003e \u003ca href=\"${result.url}\"\u003e${result.title}\u003c/a\u003e\u003cbr /\u003e \u003cp\u003e${result.content.substring(0, 150)}...\u003c/p\u003e \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; color: #6c757d; height: 1.2em; line-height: 1em; padding: 0.25em;\"\u003e \u003cdiv\u003e${result.date}\u003c/div\u003e \u003cdiv\u003e\u003ci class=\"fa fa-tags\"\u003e\u003c/i\u003e ${result.tags .map( (tag) =\u003e `\u003ca href=\"/tags/${tag}\" style=\"color: #6c757d;\"\u003e${tag}\u003c/a\u003e` ) .join(\", \")} \u003c/div\u003e \u003c/div\u003e \u003c/li\u003e`; return resultList; } Optimization Concerns Index Data Caching By default, the index is not preserved across page loads, which can result in unnecessary data fetching and processing. To improve performance, we can use localStorage to cache the search data. This caching mechanism is already implemented in the loadSearch function, where data is stored with a time-to-live (TTL) of one week. This ensures that the index is only fetched and built once a week, reducing the load on the server and improving user experience. Note that localStorage needs data to be in a serializable format and hence the index directly cannot be cached. Therefore, we are caching the index.json post data that we created in the first step and then rebuilding the index at each window creation. Limiting Search Results Limit the number of search results displayed to the user to avoid overwhelming them and to improve performance. This is done by taking a maxResults length slice above. Async Loading of Scripts To improve page load times, ensure that the search scripts are loaded asynchronously. This is achieved by adding the defer attribute to the script tags in the layout. \u003cscript src=\"{{ $lunrJS.RelPermalink }}\" defer\u003e\u003c/script\u003e \u003cscript src=\"{{ $minifiedScript.Permalink }}\" integrity=\"{{ $minifiedScript.Data.Integrity }}\" defer\u003e\u003c/script\u003e Note that this deferring means the page will first render and then the actual search execution will begin. Query-Based Search Our layout doesn’t really communicate with the script as such. It just loads the script. The script itself, sees if the data and indexes are present, then checks the URL for search query, then executes the search, modifies the html to add the result list items. It also helps to allow users to share searches easily. This is already handled in the checkURLAndSearch function, which reads the query parameter from the URL and performs a search if it’s present. Conclusion To recap, we created a search index data, a user interaction form, a search page and a layout for it. All this and lunr.js is tied together using a custom javascript. As noted before, the stylings used are bootstrap and font awesome based here, but can be easily adapted to any styling system. An implemented example of this can be found in this sites search functionality. Example search query ", + "content": "Adding search functionality to a static site generated with Hugo can significantly improve the user experience. lunr.js is a powerful JavaScript library for full-text search, offering a lightweight and fast solution ideal for static sites. In this guide, we will integrate search using lunr.js and address optimization concerns to ensure efficient and smooth operation. Note that the examples use Bootstrap and Font Awesome icons, but the same elements can be adapted to any styling system as required.\nStep 1: Create a data index for search In your config.toml, add the following lines to enable the generation of the search index file: [outputs] home = [\"HTML\", \"RSS\", \"JSON\"] [outputFormats] [outputFormats.JSON] mediaType = \"application/json\" baseName = \"index\" isPlainText = true Then, create a layouts/index.json file, that will have a template for creating the data index. This file will be processed during hugo build to create a {output dir e.g public}/index.json For example, if you want to have title, url, content, tags, date available, the template will look something like below: {{- $index := slice -}} {{- range $.Site.RegularPages -}} {{- $tags := slice }} {{- range .Params.tags -}} {{- $tags = $tags | append . }} {{- end -}} {{- $content := .Content | plainify | htmlUnescape }} {{- $datestr := .Date.Format \"Jan 2, 2006\" }} {{- $indexItem := dict \"url\" .Permalink \"title\" .Title \"content\" $content \"tags\" $tags \"date\" $datestr -}} {{- $index = $index | append $indexItem }} {{- end -}} {{- $index | jsonify (dict \"indent\" \" \") }} Step 2: Create the Search Form Now that we have the data created for search, we would need to establish a interaction mechanism with the user. You can embed the search form in the header or body of all pages or restrict it to a dedicated search page. It can be embedded as {{ partial \"search-form.html\" . }} This form should, take input from user, and on action, invoke the search page with search query embedded into the url. Below is a slightly opinionated layouts/partials/search-form.html, using Bootstrap classes and Font Awesome icons for styling. This can be adapted to any styling system. The form takes user input and invokes the search page with the search query embedded into the URL. \u003c!-- Form with a get page action --\u003e \u003cform id=\"search\" action='{{ with .GetPage \"/search\" }}{{.Permalink}}{{end}}' method=\"get\" class=\"d-flex justify-content-center mt-2 mb-4\"\u003e \u003c!-- Hidden label for accessibility --\u003e \u003clabel hidden for=\"search-input\"\u003eSearch site\u003c/label\u003e \u003cdiv class=\"input-group\" style=\"width: 90%;\"\u003e \u003c!-- Icon inside the input group for visual enhancement --\u003e \u003cspan class=\"input-group-text border-0 bg-transparent\"\u003e \u003ci class=\"fa fa-search\"\u003e\u003c/i\u003e \u003c/span\u003e \u003c!-- Search input field. The \"name\" defined here will be used to parse the URL when executing the business logic in search.js --\u003e \u003cinput type=\"text\" class=\"form-control rounded-pill\" id=\"search-input\" name=\"query\" placeholder=\"Type here to search...\" aria-label=\"Search\"\u003e \u003c!-- Submit button with an arrow icon --\u003e \u003cbutton class=\"btn border-0 bg-transparent\" type=\"submit\" aria-label=\"search\"\u003e \u003ci class=\"fa fa-arrow-right\"\u003e\u003c/i\u003e \u003c/button\u003e \u003c/div\u003e \u003c/form\u003e Step 3: Set Up Your Search Content Page To actually execute the query and display the results we need to create a search content page. Generally it would be content/search/_index.md, but this can change depending on your chosen site organization for Hugo. This file should point to a search layout, that we will create below. You can customize it as required. Typically a bare minimum file will look like: --- title: \"Search\" layout: \"search\" description: \"Search page\" --- Step 4: Create the Search Layout Now, the above page needs to be served using a layout. The search layout defines the structure of the search page and includes necessary scripts for lunr.js and the custom search logic. By including these scripts in the layout page only, we can ensure that the search functionality is loaded on this page only and doesn’t really affect other pages. This can help optimize performance for the rest of the site. Create a search.html file in your layouts/_default directory: {{ define \"main\" }} \u003cdiv id=\"search-container\" class=\"container\"\u003e \u003c!-- This is where the search results will be displayed. Initialize as empty list. --\u003e \u003cul id=\"searchresults\"\u003e\u003c/ul\u003e \u003c/div\u003e \u003c!-- Include lunr.js library. This can be included from node_modules mounted as assets/vendor, refer to their cdn or directly use from static/js. --\u003e {{ $lunrJS := resources.Get \"vendor/lunr/lunr.min.js\" }} \u003cscript src=\"{{ $lunrJS.RelPermalink }}\" defer\u003e\u003c/script\u003e \u003c!-- Include the custom search script where the magic happens. This can be used from assets/js like below, or directly from static/js. --\u003e {{ with resources.Get \"js/search.js\" }} {{ $minifiedScript := . | minify | fingerprint }} \u003cscript src=\"{{ $minifiedScript.Permalink }}\" integrity=\"{{ $minifiedScript.Data.Integrity }}\" defer\u003e\u003c/script\u003e {{ else }} {{ errorf \"search.js not found in assets/js/\" }} {{ end }} {{ end }} Step 5: Create the Search business logic script Now we need to connect all the above site elements to lunr.js, perform search and render results. We will create a javascript script for this. This script handles the entire search process, including loading the search index, processing search queries, and displaying results. The below script can be placed as assets/js/search.js and included in your search layout as shown in a previous step. Alternately, you can put it directly inside static/js folder too and include it via the search layout above. Flow of the Code Initialization: The script initializes the lunr.js search index and ensures it only happens once for a page load. Caching: It checks for cached search data in localStorage. If valid cached data is available, it uses it; otherwise, it fetches new data and caches it. Building the Index: The script constructs the lunr.js search index from the fetched data. Search Query Handling: It reads the search query from the URL parameters and triggers a search if a query is present. Search Execution: It performs the search using the built index and processes the query to ensure it is valid. Displaying Results: It limits the displayed results to a maximum of 10 to avoid overwhelming users and improve performance. // Get the search input element var searchElem = document.getElementById(\"search-input\"); // Define a global object to store search-related data and ensure it's initialized only once window.pankajpipadaCom = window.pankajpipadaCom || {}; // Initialize search only once if (!window.pankajpipadaCom.initialized) { window.pankajpipadaCom.lunrIndex = null; window.pankajpipadaCom.posts = null; window.pankajpipadaCom.initialized = true; // Load search data and initialize lunr.js loadSearch(); } // Function to load search data and initialize lunr.js function loadSearch() { var now = new Date().getTime(); // Check for cached data in localStorage var storedData = localStorage.getItem(\"postData\"); // Use cached data if available and not expired if (storedData) { storedData = JSON.parse(storedData); if (now \u003c storedData.expiry) { console.log(\"Using cached data\"); buildIndex(storedData.data, checkURLAndSearch); return; } else { console.log(\"Cached data expired\"); localStorage.removeItem(\"postData\"); } } // Fetch search data via AJAX request var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState === 4 \u0026\u0026 xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); buildIndex(data, checkURLAndSearch); console.log(\"Search initialized\"); // Cache fetched data with expiry localStorage.setItem( \"postData\", JSON.stringify({ data: data, expiry: new Date().getTime() + 7 * 24 * 60 * 60 * 1000, // TTL for 1 week }) ); } catch (error) { console.error(\"Error parsing JSON:\", error); showError(\"Failed to load search data.\"); } } else if (xhr.status !== 200) { console.error(\"Failed to load data:\", xhr.status, xhr.statusText); showError(\"Failed to load search data.\"); } }; xhr.onerror = function () { console.error(\"Network error occurred.\"); showError(\"Failed to load search data due to network error.\"); }; xhr.open(\"GET\", \"../index.json\"); xhr.send(); } // Function to build lunr.js index function buildIndex(data, callback) { window.pankajpipadaCom.posts = data; window.pankajpipadaCom.lunrIndex = lunr(function () { this.ref(\"url\"); this.field(\"content\", { boost: 10 }); this.field(\"title\", { boost: 20 }); // Define the new field for concatenated tags this.field(\"tags_str\", { boost: 15 }); this.field(\"date\"); window.pankajpipadaCom.posts.forEach(function (doc) { // Create a new field 'tags_str' for indexing const docForIndexing = { ...doc, tags_str: doc.tags.join(\" \"), }; this.add(docForIndexing); }, this); }); console.log(\"Index built at\", new Date().toISOString()); callback(); } // Function to display error message function showError(message) { var searchResults = document.getElementById(\"searchresults\"); searchResults.innerHTML = `\u003cbr\u003e\u003ch2 style=\"text-align:center\"\u003e${message}\u003c/h2\u003e`; searchElem.disabled = true; // Disable search input on error } // Function to check URL for search query and perform search function checkURLAndSearch() { var urlParams = new URLSearchParams(window.location.search); var query = urlParams.get(\"query\"); if (query) { searchElem.value = query; showSearchResults(); } } // Function to perform search and display results function showSearchResults() { if (!window.pankajpipadaCom.lunrIndex) { console.log(\"Index not available.\"); return; // Exit function if index not loaded } var query = searchElem.value || \"\"; var searchString = query.trim().replace(/[^\\w\\s]/gi, \"\"); if (!searchString) { displayResults([]); return; // Exit if the search string is empty or only whitespace } var matches = window.pankajpipadaCom.lunrIndex.search(searchString); console.log(\"matches\", matches); var matchPosts = matches.map((m) =\u003e window.pankajpipadaCom.posts.find((p) =\u003e p.url === m.ref) ); console.log(\"Match posts\", matchPosts); displayResults(matchPosts); } // Function to display search results function displayResults(results) { const searchResults = document.getElementById(\"searchresults\"); const maxResults = 10; // Limit to 10 results if (results.length) { let resultList = \"\"; results.slice(0, maxResults).forEach((result) =\u003e { if (result) { resultList += getResultStr(result); } }); searchResults.innerHTML = resultList; } else { searchResults.innerHTML = \"No results found.\"; } } // Function to format search result items function getResultStr(result) { var resultList = ` \u003cli style=\"margin-bottom: 1rem\"\u003e \u003ca href=\"${result.url}\"\u003e${result.title}\u003c/a\u003e\u003cbr /\u003e \u003cp\u003e${result.content.substring(0, 150)}...\u003c/p\u003e \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; color: #6c757d; height: 1.2em; line-height: 1em; padding: 0.25em;\"\u003e \u003cdiv\u003e${result.date}\u003c/div\u003e \u003cdiv\u003e\u003ci class=\"fa fa-tags\"\u003e\u003c/i\u003e ${result.tags .map( (tag) =\u003e `\u003ca href=\"/tags/${tag}\" style=\"color: #6c757d;\"\u003e${tag}\u003c/a\u003e` ) .join(\", \")} \u003c/div\u003e \u003c/div\u003e \u003c/li\u003e`; return resultList; } Optimization Concerns Index Data Caching By default, the index is not preserved across page loads, which can result in unnecessary data fetching and processing. To improve performance, we can use localStorage to cache the search data. This caching mechanism is already implemented in the loadSearch function, where data is stored with a time-to-live (TTL) of one week. This ensures that the index is only fetched and built once a week, reducing the load on the server and improving user experience. Note that localStorage needs data to be in a serializable format and hence the index directly cannot be cached. Therefore, we are caching the index.json post data that we created in the first step and then rebuilding the index at each window creation. Limiting Search Results Limit the number of search results displayed to the user to avoid overwhelming them and to improve performance. This is done by taking a maxResults length slice above. Async Loading of Scripts To improve page load times, ensure that the search scripts are loaded asynchronously. This is achieved by adding the defer attribute to the script tags in the layout. \u003cscript src=\"{{ $lunrJS.RelPermalink }}\" defer\u003e\u003c/script\u003e \u003cscript src=\"{{ $minifiedScript.Permalink }}\" integrity=\"{{ $minifiedScript.Data.Integrity }}\" defer\u003e\u003c/script\u003e Note that this deferring means the page will first render and then the actual search execution will begin. Query-Based Search Our layout doesn’t really communicate with the script as such. It just loads the script. The script itself, sees if the data and indexes are present, then checks the URL for search query, then executes the search, modifies the html to add the result list items. It also helps to allow users to share searches easily. This is already handled in the checkURLAndSearch function, which reads the query parameter from the URL and performs a search if it’s present. Conclusion To recap, we created a search index data, a user interaction form, a search page and a layout for it. All this and lunr.js is tied together using a custom javascript. As noted before, the stylings used are bootstrap and font awesome based here, but can be easily adapted to any styling system. An implemented example of this can be found in this sites search functionality. Example search query ", "date": "May 27, 2024", "tags": [ "hugo" diff --git a/index.xml b/index.xml index 396af87..be7b0ae 100644 --- a/index.xml +++ b/index.xml @@ -7,7 +7,7 @@ Hugo -- gohugo.io en Copyright © 2016-2024 Pankaj Pipada. All Rights Reserved. - Mon, 27 May 2024 18:53:41 +0530 + Mon, 27 May 2024 19:05:21 +0530 Hugo - Integrating Datatables diff --git a/js/search.min.7e968a9ef35980108f5b9b4eed6c8b672abb5b753bdca314ba6b6f8b07fc5768.js b/js/search.min.7e968a9ef35980108f5b9b4eed6c8b672abb5b753bdca314ba6b6f8b07fc5768.js new file mode 100644 index 0000000..f0157a0 --- /dev/null +++ b/js/search.min.7e968a9ef35980108f5b9b4eed6c8b672abb5b753bdca314ba6b6f8b07fc5768.js @@ -0,0 +1,11 @@ +var searchElem=document.getElementById("search-input");window.pankajpipadaCom=window.pankajpipadaCom||{},window.pankajpipadaCom.initialized||(window.pankajpipadaCom.lunrIndex=null,window.pankajpipadaCom.posts=null,window.pankajpipadaCom.initialized=!0,loadSearch());function loadSearch(){var e,n=(new Date).getTime(),t=localStorage.getItem("postData");if(t){if(t=JSON.parse(t),n

${e}

`,searchElem.disabled=!0}function checkURLAndSearch(){var t=new URLSearchParams(window.location.search),e=t.get("query");e&&(searchElem.value=e,showSearchResults())}function showSearchResults(){if(!window.pankajpipadaCom.lunrIndex){console.log("Index not available.");return}var e,t,s=searchElem.value||"",n=s.trim().replace(/[^\w\s]/gi,"");if(!n){displayResults([]);return}e=window.pankajpipadaCom.lunrIndex.search(n),console.log("matches",e),t=e.map(e=>window.pankajpipadaCom.posts.find(t=>t.url===e.ref)),console.log("Match posts",t),displayResults(t)}function displayResults(e){const t=document.getElementById("searchresults");if(e.length){let n="";e.forEach(e=>{e&&(n+=getResultStr(e))}),t.innerHTML=n}else t.innerHTML="No results found."}function getResultStr(e){var t=` +
  • + ${e.title}
    +

    ${e.content.substring(0,150)}...

    +
    +
    ${e.date}
    +
    + ${e.tags.map(e=>`${e}`).join(", ")} +
    +
    +
  • `;return t} \ No newline at end of file diff --git a/posts/2024-05-27-hugo-search/index.html b/posts/2024-05-27-hugo-search/index.html index 038b873..78e3957 100644 --- a/posts/2024-05-27-hugo-search/index.html +++ b/posts/2024-05-27-hugo-search/index.html @@ -357,11 +357,17 @@