How to Add Search to a Hugo Site
Hugo is fast, flexible, and opinionated in all the right ways — but it ships without built-in search. Since Hugo generates a static site, there is no server-side query engine to call. Search has to be handled either at build time, client-side in the browser, or through a third-party service. Each approach has tradeoffs worth understanding before you commit to one.
This guide covers the main options and walks through the implementation that works best for most publishing workflows.
Why Hugo Has No Native Search
Static site generators produce a flat collection of HTML files. When a visitor loads a page, there is no application server processing the request — just a file being served from disk or a CDN. That model makes Hugo sites extremely fast and cheap to host, but it also means any dynamic behavior, including search, has to live elsewhere.
The practical consequence: search on a Hugo site is almost always a JavaScript problem.
Option 1: Pagefind (Recommended)
Pagefind is a static search library designed specifically for built sites. After you run hugo, you point Pagefind at your public/ directory and it crawls the HTML, builds a compact binary index, and writes a self-contained search UI to the output folder. No API key, no external service, no ongoing cost.
Install and index:
hugo
npx pagefind --site public
That’s the full build pipeline. Pagefind writes its index and UI assets into public/pagefind/.
Add the UI to your layout:
Place this wherever you want the search widget to appear — typically a dedicated search page or in your base template:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
new PagefindUI({ element: "#search" });
</script>
Why it works well for publishers:
- Indexes are generated incrementally and are surprisingly small — a site with thousands of posts typically produces an index well under a few megabytes
- Search runs entirely in the browser; no query leaves the visitor’s machine
- Supports multilingual sites out of the box
- Results include excerpts with highlighted match terms
The only friction is build pipeline integration. If you deploy via a CI service (Netlify, GitHub Actions, Cloudflare Pages), add the npx pagefind step after hugo build in your workflow config.
Netlify example (netlify.toml):
[build]
command = "hugo && npx pagefind --site public"
publish = "public"
Option 2: Fuse.js with a JSON Index
Fuse.js is a lightweight fuzzy-search library that runs entirely in the browser against a JSON payload you generate at build time. It is a good fit for smaller sites; on larger archives the JSON index can get heavy.
Step 1 — Enable JSON output in hugo.toml:
[outputs]
home = ["HTML", "RSS", "JSON"]
Step 2 — Create layouts/index.json:
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict
"title" .Title
"url" .RelPermalink
"summary" .Summary
"content" .Plain
"tags" .Params.tags
) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
This outputs a index.json at your site root on every build.
Step 3 — Load and query on a search page:
<input id="search-input" type="text" placeholder="Search...">
<ul id="results"></ul>
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
<script>
fetch('/index.json')
.then(r => r.json())
.then(data => {
const fuse = new Fuse(data, {
keys: ['title', 'summary', 'content', 'tags'],
threshold: 0.3
});
document.getElementById('search-input').addEventListener('input', e => {
const results = fuse.search(e.target.value).slice(0, 10);
document.getElementById('results').innerHTML = results
.map(r => `<li><a href="${r.item.url}">${r.item.title}</a></li>`)
.join('');
});
});
</script>
Option 3: Algolia
Algolia is a hosted search service with generous relevance tuning, typo tolerance, and analytics. The free tier covers up to 10,000 records and 10,000 requests per month — workable for small to mid-size publishing sites. For documentation or open-source projects, DocSearch is free.
The tradeoff is operational overhead: you need to push your content index to Algolia on every deploy, manage API keys, and depend on a third-party service for your search to function.
Basic workflow:
- Sign up and create an Algolia index
- Use the atomic-algolia npm package or a custom script to push your Hugo JSON output to Algolia on deploy
- Use Algolia’s InstantSearch.js library to render results
Algolia is worth the complexity if you need analytics on what visitors are searching for, or if you want fine-grained control over result ranking.
Option 4: Lunr.js
Lunr is a mature client-side search library that pre-builds a serialized index. It is heavier than Fuse.js for a similar use case, but produces more precise results for exact-term matching. The pattern is the same as Fuse.js — generate JSON at build time, load it in the browser, query against the Lunr index.
fetch('/index.json')
.then(r => r.json())
.then(data => {
const idx = lunr(function() {
this.ref('url');
this.field('title', { boost: 10 });
this.field('content');
data.forEach(doc => this.add(doc));
});
// query: idx.search('your term')
});
Choosing the Right Approach
| Approach | Best for | Tradeoff |
|---|---|---|
| Pagefind | Most Hugo publishing sites | Requires build pipeline step |
| Fuse.js | Small sites, simple setup | Large JSON on big archives |
| Algolia | High-traffic sites needing analytics | External dependency, API keys |
| Lunr.js | Precise term matching | Larger bundle, more setup |
For publishing operations running Hugo at any meaningful scale, Pagefind is the right default. It requires no external account, produces small indexes, handles large post archives cleanly, and the implementation is a two-line build command and a three-line HTML snippet. Start there, and reach for Algolia only if you outgrow it.