改善
Kaizen  · Today I Learned by Ville Säävuori

Adding Simple Search to Hugo

I’ve beeen looking for search options for static sites for a while now and haven’t found anything I’d really like. I wanted to test out buildinga native Web Component with Vue and Vite but in the end I decided to use native Vue component instead.

You can test the end result on the homepage.

Add Search To Hugo Site in 5 Minutes

Using modern JS stacks with most static site generators is not great and Hugo isn’t an exception. Hugo has pipelines which kind of work but not too well. I built a simple Vue component that you can use with Hugo pipelines and get a self hosted search working with your Hugo site in minutes.

The following workflow is based on this Gist but adapted to use a much cleaner and easier to maintain Headless Vue JSON Search component.

The idea is to use Hugo pipelines to compile the needed JavaScript and let Hugo generate the search corpus automatically on every build.

1. Install Vue + Search Component

If you have package.json in your project, just npm or yarn install vue@next and vue-json-search. (yarn add vue@next vue-json-search)

(If you don’t, init the project first (yarn init).)

2. Create The Search Script

Create the following sccript somewhere in your theme. Mine is at themes/mytheme/assets/js/search.js.

This script loads Vue and the search component, and you can add whatever JavaScript you want to it. It’s just plain JavaScript.

import { createApp, h } from 'vue'
import { JsonSearch } from 'vue-json-search'

createApp({
  render: () => h(JsonSearch, { showTags: true }), // Props argument dict is optional
}).mount('#searchapp')

As the search component is a Vue component, but we don’t want to use Vue templates or other features, we just create a simple Vue app, and use the render function to render the search component. The idea here is to keep the JavaScript as small as simple as possible.

3. Add Search Markup to HTML Template

I added the following to homepage template at themes/mytheme/layouts/index.html:

<div>
    <h2>Search</h2>
    <div id="searchapp"></div>
</div>

When the JavaScript gets executed, Vue will replace the #searchapp div with the search component. Again, you can use whatever markup here, just make sure it makes sense for people who don’t have JavaScript enabled.

4. Add Search Script to HTML template

You’ll want to include the script somewhere near the </body>-tag of the page(s) you are using the search.

The following code takes the search script, bundles it with Hugo pipelines, and in production adds a integrity hash to it. (You can also minify with Pipelines but I couldn’t get it working for my site so I just left it out.)

{{ if  hugo.IsProduction  }}
{{ $securejs := resources.Get "js/search.js" | js.Build | resources.Fingerprint "sha512" }}
<script type="text/javascript" src="{{ $securejs.RelPermalink }}" integrity="{{ $securejs.Data.Integrity }}" defer></script>
{{- else }}
{{ $builtjs := resources.Get "js/search.js" | js.Build }}
<script type="text/javascript" src="{{ $builtjs.RelPermalink }}" defer></script>
{{- end }}

5. Configure Hugo to Create JSON Search Corpus

First, in confog.toml make sure you have JSON configured:

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

Then, add following themes/[your-theme]/layouts/_default/index.json:

{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
    {{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "contents" .Plain "permalink" .Permalink) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

This adds title, tags, and content to the search corpus. You can tweak this to your needs and then configure the Vue search component to match.

That’s all — now when you restart youd Hugo dev server, you should see a working search!

6. Tweak Your Styles

The search component doesn’t ship with any styles but it does have a simple markup that is very easy to style. I used Tailwind fot this blog, my PostCSS styles look like this:

.jsonsearch {
  @apply mt-6 w-full;
}

.jsonsearch label {
  @apply sr-only;
}

.jsonsearch .jsonsearchinput {
  @apply flex bg-gray-800 px-2 py-1 text-gray-100 placeholder:text-gray-500 mx-auto w-5/6 md:w-1/2 md:mx-0;
}

.jsonsearch .searchresults h3 {
 @apply my-4 text-lg font-semibold;
}

.jsonsearch .searchresults ol {
 @apply space-y-4;
}

.jsonsearch .searchresults .title a {
 @apply inline-block text-lg leading-none text-indigo-400 align-top hover:underline;
}

.jsonsearch .result .tags {
 @apply block text-gray-400 text-xs;
}

.jsonsearch .result .tags a {
 @apply leading-tight text-gray-400 text-opacity-80 hover:text-gray-200;
}

Conclusion

This is a pretty easy way to add a modern and very easy to maintain search to any site that can output a JSON search corpus.

A simple search like this that loads the whole corpus in memory obviously doesn’t work for gigantic sites. I wouldn’t want to load anything over a couple of megabytes with this system, although the script won’t slow down your site at all as it’s deferred and the search only shows up after everything is loaded and inited.

The whole search script including full Vue dependency is only 188 Kb unzipped, which for my uses is not too much. It’s not nothing either, but this site has no other JavaScript whatsoever so the added loading time is worth it. If you need more optimized solution, you can either write all the JavaScript by hand (saving ~30k of Vue) or use some third-party tool and/or external search service.