# Simple Hugo search using JSON file

**URL:** <https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964>\
**Category:** tips & tricks\
**Created:** [February 3, 2022, 11:51pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964 "2022-02-03T23:51:53Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![toledo](https://avatars.discourse-cdn.com/v4/letter/t/ecd19e/32.png) [@toledo](https://discourse.gohugo.io/u/toledo)\
**Post date:** [February 3, 2022, 11:51pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/1 "2022-02-03T23:51:53Z")

</div>

I tweaked my code below [from this tutorial](https://nurofsun.com/search-feature-on-hugo/). This is for Hugo beginners mostly (like me 😁).

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

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

```

And in YAML format:

```auto
outputs:
  home:
    - HTML
    - RSS
    - JSON

```

1. Create an `index.json` file in the root of layouts folder and add the code below.

```auto
[
  {{ $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](https://discourse.gohugo.io/t/help-needed-difference-between-plain-plainwords-and-plainify/5681/2). 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:

```auto
---
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>`

```auto
// 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!

---

<div class="post-metadata">

**Author:** ![Horbes](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@Horbes](https://discourse.gohugo.io/u/Horbes)\
**Post date:** [February 5, 2022, 10:08pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/2 "2022-02-05T22:08:42Z")

</div>

It’s great to see things like this. Well done. You might interested to know that someone posted [a similar search function](https://discourse.gohugo.io/t/a-simple-javascript-based-full-text-search-function/29119) 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:

```auto
[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.

---

<div class="post-metadata">

**Author:** ![toledo](https://avatars.discourse-cdn.com/v4/letter/t/ecd19e/32.png) [@toledo](https://discourse.gohugo.io/u/toledo)\
**Post date:** [February 5, 2022, 10:35pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/3 "2022-02-05T22:35:25Z")

</div>

> [@Horbes](#):
>
> I think your solution may also require another config file change to allow HTML in Markdown files though:
> 
> ```auto
> [markup]
> [markup.goldmark.renderer]
> unsafe = true
> 
> ```

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

> [@Horbes](#):
>
> 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.

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`.

> [@Horbes](#):
>
> Or just use an HTML file instead perhaps.

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

---

<div class="post-metadata">

**Author:** ![toledo](https://avatars.discourse-cdn.com/v4/letter/t/ecd19e/32.png) [@toledo](https://discourse.gohugo.io/u/toledo)\
**Post date:** [February 5, 2022, 11:01pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/4 "2022-02-05T23:01:04Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![Horbes](https://avatars.discourse-cdn.com/v4/letter/h/a9adbd/32.png) [@Horbes](https://discourse.gohugo.io/u/Horbes)\
**Post date:** [February 17, 2022, 8:29am UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/5 "2022-02-17T08:29:53Z")

</div>

> [@toledo](#):
>
> 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` .

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.

---

<div class="post-metadata">

**Author:** ![zwbetz](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/zwbetz/32/16088_2.png) [@zwbetz](https://discourse.gohugo.io/u/zwbetz)\
**Post date:** [February 17, 2022, 1:21pm UTC](https://discourse.gohugo.io/t/simple-hugo-search-using-json-file/36964/6 "2022-02-17T13:21:45Z")

</div>

@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](https://github.com/zwbetz-gh/zwbetz/blob/88cd5e721050c2a4c04e549e7377112455c6e43a/themes/feather/assets/js/search.js)
