Implementing local site search in Hugo using Jets.js

Jul 06, 2021 by Kolappan N

When I first implemented search for my blog I went straight for the good old Google custom search. After all, I did my BE final project based on it and I already know how to set it up for my blog. The search results were good and accurate and it was good functionally. But it never fit into my site aesthetically. I customised the colours and all to make the look and feel match my site to some extent, but it still like a widget attached from the internet.

First I looked into Algolia. It is a good search engine and it can work on static sites. You’ll need to update your search index everytime you update your content but those process can be automated.

Then I came across Lunar js. The whole concept of local search engine was of great interest to me. The concept is simple, the search index will be downloaded along with the site and all searched will happen locally mostly using js. My site is a relatively small site and I could use a local site search. It’ll be easy to implement and the UI is perfectly match my site and the content will be updated automatically.

I searched various local search libraries and found Jets.js to be the best suitable for me.

How it works

The main difference between Jets.js and other local search engines how they show search results. Most local search libraries need an indexed json file that contains all the search terms & other data. They show the search results based on the index and the search keyword, mostly using js.

But Jets is different, it will list all search results first. As the user types into the search box, the results that don’t match the typed query will be hidden using CSS. I like this way better.

Implementing Jets search in Hugo

  1. You’ll need a page that lists all blog post. It could be your blog posts list, but if you have pagination it won’t work as Jets only searches the content in the page. I created a similar page with less content and without pagination.
  2. I created a layout file for search section which is as follow,
{{define "main"}}
    Powered by Jets, a CSS based search
<input class="form-control" type="search" placeholder="Type to search" id="jetsSearch">
<ol class="mt-4" id="jetsContent">
    {{range .Site.Pages }}
    {{if .Title}}
    {{if eq .Section "blog" }}
    <li class="mt-2">
        <a id="jetsContent" href="{{.Permalink}}">{{.Title}}</a>
  1. I then created a file in content folder with the following contents.
title: Search Results
layout: search
noIndex: true

Search page for Powered by

This is a dummy file used to generate Search page. The actual layout is here: \src\layouts\default\search.html
  1. Add the jets.js script to your website.
  2. If you have a dedicated search page, map the search query to the search input.
{{ if eq .Layout "search"}}
{{ $js := resources.Get "/scripts/jets.min.js" | resources.Fingerprint "sha512" }}
<script type="text/javascript" src="{{ $js.Permalink }}">
<script defer>
    const urlParams = new URLSearchParams(;
    const searchQuery = urlParams.get('q');
    var jets = new Jets({
        searchTag: '#jetsSearch',
        contentTag: '#jetsContent'
    document.getElementById('jetsSearch').value = searchQuery;;


At the time of writing the lighthouse score of the search page is the same as the rest of the website. The js is a tiny script(8 KB) doesn’t impact performance much and if you don’t have a search query to link to from the URL like me, you can fully defer it.

Lighthouse score of search screen
Lighthouse score of search screen

However the size of the HTML increases with each blog post you add. For 50 blog post the HTML size (with title & summary) is about 28 KB and for 500 posts it is about 241 KB.

I am pretty happy with this performance. But if you have a large site with let’s say more than 5000 pages you are better off with search services like Algolia.

Content Security Policy

Jets.js does use inline styles, but I found a way to add nonce id to it. See this pull request for more details. Hopefully, this gets merged into the core repo.