Simple Hugo search using JSON file

I tweaked my code below from this tutorial. This is for Hugo beginners mostly (like me :grin:).

Before you begin, add this code to your configuration file. I use TOML, so mine looks like this:

[outputs]
	home = ["HTML","RSS","JSON"]

And in YAML format:

outputs:
  home:
    - HTML
    - RSS
    - JSON
  1. Create an index.json file in the root of layouts folder and add the code below.
[
  {{ $post := where site.RegularPages "Type" "post" }}
  {{ range $index, $page :=  $post }}
      {{ if $index }},{{ end }}
        {
            "url": {{ $page.RelPermalink | jsonify }},
            "title": {{ $page.Title | jsonify}},
            "content": {{ $page.Content | jsonify }}
        }
    {{ end }}
]

Edit it to fit your needs, e.g changing the $post parameter or for "content", you can use either of these terms. Test if it generates any content by adding /index.json at the end of your domain/localhost.

  1. Create a page inside the content folder (in the section containing your pages) to host your search form. I called mine search.md. Add this code to it:
---
title: Search Page
url: /search/
_build:
 list: never
---
<div class="search-box">
 <input class="input" id="searchInput" type="text" placeholder="press '/' to search">
 <div id="searchResult">
  <!-- the search result will appear here -->
 </div>
</div>
  1. Create a search.js file where you store your static files (usually ‘static’ or ‘assets’ folder), add the code below, then load it in your template’s footer e.g <script type="text/javascript" src="search.js"></script>
// Begin search.js
let searchInput = document.querySelector('#searchInput'),
    searchResult = document.querySelector('#searchResult');

let dataJSON;

// add keydown listener, when user hit '/', it will focus on search input (Desktop)
window.addEventListener('keydown', function(event) {
    if (event.key === '/') {
        event.preventDefault()
        searchInput.focus()
    }
})
// add keydown listener, when user hit 'ESC', it will close search result and unfocus search input.
window.addEventListener('keydown', function(event) {
    if (event.keyCode === 27)
    {
        searchInput.value = '';
        searchResult.innerHTML = '';
        searchInput.blur()
    }
})
/**
 * Get the posts lists in json format.
 */
const getPostsJSON = async () => {
    let response = await fetch('/index.json')
    let data = await response.json()
    return data
}
/**
 * @param query, element.
 * query: the keyword that the user gives.
 * element: target element to show the result.
 */
const filterPostsJSON = (query, element) => {
    let result, itemsWithElement;
    query = new RegExp(query, 'ig')
    result = dataJSON.filter(item => query.test(item.content))
    itemsWithElement = result.map(item => (
        `<div class="search-result"><h2><a href="${item.url}">${item.title}</a></h2><p>${item.content}</p></div>`
    ))
    itemsWithElement.unshift(`<p>To cancel search, Press 'ESC'</p>`)
    element.innerHTML = itemsWithElement.join('');
}
/**
 * searchInputAction take two arguments, event and callback
 */ 
const searchInputAction = (event, callback) => {
    searchInput.addEventListener(event, callback)
}
/**
 * When the user focuses on the search input, the function getPostsJSON is called.
 */
searchInputAction('focus', () => getPostsJSON().then(data => dataJSON = data))
/**
 * filtering result with the query that user given on search input.
 */
searchInputAction('keyup', (event) => filterPostsJSON(event.target.value, searchResult))

Change the heading h2 here to whatever you like e.g div <div class="search-result"><h2><a href="${item.url}">${item.title}</a></h2><p>${item.content}</p></div>

You can now test to see if your search form works, then style it to your liking with CSS. Cheers!

6 Likes

It’s great to see things like this. Well done. You might interested to know that someone posted a similar search function a couple of years ago. I’ve used that one and found it very handy and easy to set up too.

I think your solution may also require another config file change to allow HTML in Markdown files though:

[markup]
    [markup.goldmark.renderer]
      unsafe = true

As I understand it this is fine for personal sites but not such a good idea where other people are edting the site as a stray bit of malformed HTML could mess up the whole page layout.

A safer alternative would be to leave out the above config code (so then the default unsafe=false is set) and add the HTML to a shortcode and then use that in the Search page markdown file. Or just use an HTML file instead perhaps.

Anyway good job making and sharing this and I will give it try next time I need a search like this.

1 Like

True. Anyone who implements this tutorial should add this option to the config file.

The shortcode will still need the unsafe option set to true for markdown files. I have a TOC shortcode and it does not work with unsafe set to false.

Good idea. I renamed mine from search.md to search.html.

Also, it is worth noting that this method is only suitable for sites with a small number of posts (up to a few hundred). In my case, with about 200 posts, the JSON file (with summary) is just about 116kb while with full content it is about 1MB in size. The bigger the file, the more impact (your site speed) it will have on your visitors, especially those on slow networks because the file will be downloaded on their device at least once.

That’s sounds like errant behaviour. Shortcodes are meant for adding HTML to markdown files without changing the default config options, ie unsafe = false. Not sure why that should be so and not experienced that myself.

@toledo

My previous search implementation ran into this size issue too.

If you’re ever looking for alternatives in the future, I ended up removing the JSON index (since it was the bulk of the size).

Instead, vanilla JS is used to grab the posts from the DOM then filter them.

It’s definitely not as clean as the JSON index way, and it depends on your HTML being structured in a certain way.

But it’s faster. And the only size increase is the small JS file