Hugo で作った静的サイトに検索を実装してみた(完成品)。
サイトのテキスト全てを JSON で出力してダウンロードさせ、それを JavaScript で検索する、というのが常套らしいので、そうしている。また、検索には lunr.js というライブラリを用いるのが人気なようだが、今回は自分で書いてみた。ナイーヴに .indexOf
で線形探索してるだけなので、単語区切りとかそういったものは一切考慮していないが、まあそんな問題になることはないと思う。
検索結果の表示には Vue.js を使おうと思ったが、たかだか 1 ページのためだけに Vue.js の開発環境を丸ごと用意するのも癪だったので、生の DOM API を叩くというゴリ押しで突破した。
また、fcitx-skk 環境下で使いやすいように、クエリの先頭に「▽」か「▼」があったらそれを無視するようにした。便利。
以下手順のメモ。
JSON の出力
layouts/_default/list.json
に出力したい JSON のテンプレートを書く。テンプレートとは言いつつ、実際は「リストを定義し、記事データを挿入し、JSON として出力する」というプログラムみたいな感じになっている。
{{- $.Scratch.Add "index" slice -}}
{{- range .Pages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "text" .Plain "created_at" .Date "url" .Permalink) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
テンプレートの形式でロジックが書かれていて見づらいが、疑似コードにするとこんな感じ。
index = []
for (page in pages) {
index.push({
title: page.title, text: page.plain, created_at: page.date, url: page.permalink
})
}
print(index.toJSON())
config.toml
で JSON もビルドするように設定する。
[outputs]
home = ["HTML", "JSON"]
これで、サイトのビルド時に全テキストが入った JSON が生成される。実際のJSONはこちら。
検索ページの作成
検索ページ は普通の Hugo ページとして実装するので、まずは Front Matter だけの content/search.md
を作り、以下の内容を記述する。日付は適当。
---
title: "Search"
date: 2020-12-21T18:57:04+09:00
type: search
draft: false
---
次に、 type: search
のとき用のテンプレートを layouts/search/single.html
に書く。検索のロジックも書く。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>検索</title>
</head>
<body>
検索ページのコンテンツいろいろ
<script>
検索スクリプトいろいろ
</script>
</body>
</html>
こうすると、/search
にアクセスしたときだけこの HTML が読まれるようになる。
記事一覧に検索ページを出さない
このままだと、トップページの記事リストに検索ページも表示されてしまう。search.md
の Front Matter に unlisted: true
を追記。
---
title: "Search"
date: 2020-12-21T18:57:04+09:00
type: search
draft: false
unlisted: true
---
トップページのテンプレートで unlisted: true
の記事を出さないようにする。
{{ range .Pages }}
{{ if not .Params.unlisted }}
<li>
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
<time>({{ .Date }})</time>
</li>
{{ end }}
{{ end }}
おわり。