Search Index
gohan generates a search-index.json at build time so you can add client-side article search to your theme without any external tooling or services.
日本語版: search-index.ja.md
How it works
During the build, gohan writes a search-index.json alongside sitemap.xml and atom.xml in the "feeds" phase. It is generated automatically — no configuration is required.
Each entry holds metadata only (no full body text), so the index stays small even on sites with hundreds of articles:
| Field | Example | Description |
|---|---|---|
title |
Hello, World! |
Article title |
url |
https://example.com/posts/hello/ |
Absolute URL (same rule as feeds) |
description |
A short intro |
Meta description |
summary |
An auto-generated excerpt… |
Article summary |
tags |
["go", "ssg"] |
Tags (omitted when empty) |
categories |
["features"] |
Categories (omitted when empty) |
date |
2026-03-14T00:00:00Z |
Publish date (RFC 3339) |
locale |
en |
Locale code (present only with i18n) |
Articles are sorted newest-first. Draft and future-dated articles are excluded unless you pass --draft or --future.
Output shape
{
"generated": "2026-06-27T04:57:01Z",
"count": 2,
"articles": [
{
"title": "i18n",
"url": "https://bmf-san.github.io/gohan/features/i18n/",
"description": "Build multi-language sites with directory-based locales.",
"summary": "## Overview",
"categories": ["features"],
"locale": "en"
}
]
}
i18n
When i18n.locales is configured, the index is split per locale — the same convention used by feeds and sitemap:
- The root
search-index.jsoncontains default-locale articles only. - Each non-default locale gets its own
{locale}/search-index.json(e.g.ja/search-index.json).
Each localized page can then load only the entries for its own language.
Theme usage
The index is a static JSON file, so a few lines of vanilla JavaScript are enough — no dependencies required:
<input id="q" type="search" placeholder="Search…">
<ul id="results"></ul>
<script>
const out = document.getElementById("results");
fetch("/search-index.json")
.then((r) => r.json())
.then(({ articles }) => {
document.getElementById("q").addEventListener("input", (e) => {
const q = e.target.value.toLowerCase().trim();
out.replaceChildren();
if (!q) return;
for (const a of articles) {
const hay = [a.title, a.description, (a.tags || []).join(" ")]
.join(" ")
.toLowerCase();
if (!hay.includes(q)) continue;
const li = document.createElement("li");
const link = document.createElement("a");
link.href = a.url; // set as a property — avoids HTML injection
link.textContent = a.title; // textContent escapes the title safely
li.appendChild(link);
out.appendChild(li);
}
});
});
</script>
For fuzzy matching or ranking, pass articles to a small client-side library such as MiniSearch or Fuse.js.
When your site is served from a sub-path (e.g. GitHub Pages at
/gohan), prefix the fetch URL with yourbase_path.
Internals
The logic lives in internal/generator/searchindex.go, mirroring how feeds and sitemap are generated.
| File | Change |
|---|---|
internal/generator/searchindex.go |
GenerateSearchIndex: writes metadata-only entries, newest-first, with an i18n-aware split |
cmd/gohan/build.go |
Calls GenerateSearchIndex in the build "feeds" phase |
internal/generator/searchindex_test.go |
Unit tests: valid output, empty site, field mapping, i18n split |