kumpei.ikuta.me

Hugo サイトに全文検索つけた

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 }}

おわり。

back to index