2024-06-23

blog-fable に pagefind による検索を追加する

krymtkts/pocof 放置気味だが、今日も krymtkts/blog-fable の話を書く。

このブログは結構自身の備忘のために役立っている。 例えば chocolatey で OpenSSH をインストールするオプションを確認するときのような、たまにしかやらないことを探すのにとても役立つ。 ただ書くときは探しやすさに注意を払わずタイトルを書いてることもあって、どこに書いたか忘れて、あとからそういった情報を探すのにいちいち数記事読み返すときもある。 そんなときは検索機能が欲しくなるのだけど、あんまり日本語がいい感じで、軽量な静的コンテンツの検索ってなさそうだったので放置していた。

でも最近 pagefind の日本語が良くなってきていると知り、また導入も簡単だということなのでやってみた。


blog-fable に pagefind を導入するのに以下を行った。

  • task runner に pagefind の実行を追加する

  • 検索を最適化するための設定を作成し、 metadata を追加する

  • pagefind UI の初期化コードを仕込む

  • pagefind UI のスタイル調整する

task runner に pagefind の実行を追加する

まず pagefind の実行だが、やり方は至極簡単。 概ね pagefind の文書に書いてあるとおりに index を構築して GitHub Pages に deploy したらいいだけ。

Running Pagefind | Pagefind — Static low-bandwidth search at scale

当然静的コンテンツの作成後出ないといけないので、コマンドの実行順にだけ気をつけたらいい。 こんなふうにした。 元々 build task に引数を渡して開発と本番を切り分けてたが、直接引数を渡すと最後尾の build-index に適用されてしまうため、仕方なく分割した。

  "scripts": {
"postinstall": "dotnet tool restore",
"build-fable": "dotnet fable src --test:MSBuildCracker --runScript",
"build-md": "node ./src/App.fs.js",
"build-css": "sass --style=compressed --no-source-map ./sass/style.scss ./docs/blog-fable/css/style.css",
"build-index": "pagefind",
"build": "npm run build-css && npm run build-fable && npm run build-index",
"build-dev": "npm run build-css && npm run build-fable dev && npm run build-index",
"serve": "dotnet fsi ./dev-server.fsx /blog-fable",
"dev": "npm run build-dev && npm run serve"
},

検索を最適化するための設定を作成し、 metadata を追加する

次は重要な点で、静的コンテンツからどう index 構築するかの設定する必要があった。 デフォルトだと何もかも indexing してしまうので、どの要素で構築すべきか、どのパスで構築すべきかの設定しないと検索のノイズがすごかった。 幸い pagefind は設定ファイルをサポートしているので、このような面倒な設定はすべてファイルにまとめられる。 こんなふうにした。

site: ./docs/blog-fable
root_selector: .content
keep_index_url: true
glob: "{posts,pages}/*.html"

site は対象となる静的コンテンツのディレクトリ。 glob で指定するパターンと組み合わせて対象の静的コンテンツを指定できる。 blog-fable は posts/ pages/ 以外を対象にしたくない(404 だったり archives だったりは不要)のでこのオプションがなかったら終わってた。

blog-fable では root_selector を使ったが、文書では data-pagefind-body を使うべきで root_selector は余り使う必要ないと書いてあった。 ただ blog-fable は Markdown から翻訳された HTML はすべて .content 配下に出力されるので、むしろこのオプションの方がハマっていた。 一箇所だけ front matter なし page だと title 要素を拾えなかったので、しゃーなしで metadata data-pagefind-meta="title" を追加した。 こんなふうに

                let meta = if l = 2 then " data-pagefind-meta=\"title\" " else ""

$"""<h%d{l} %s{meta}><a name="%s{escapedText}" href="#%s{escapedText}">%s{text}</a></h%d{l}>"""

keep_index_urlindex.html を取り除くかどうか。 blog-fable では top page 以外にないので不要だが、万が一当てはまるパターンが増えたときに加工されるのはかなんのでつけた。

pagefind UI の初期化コードを仕込む

次はサイトで検索を使うための初期化コードを仕込む。 オプションの指定で検索に表示される文言を変えたりするので、指定項目は多いが一通り指定するのが無難だろう。

ここで注意すべき設定項目は baseUrl の指定だ。 blog-fable のような GitHub アカウントの page じゃない場合 URL が https://${account}.github.io/${repo-name}/ となる。 そのためそれに合わせて baseUrl を設定する必要がある。 このブログなら GitHub アカウントの page なので "/" で、 blog-fable は "/blog-fable/" だ。

あと blog-fable 固有の対応として、 初期化の JavaScript を F# に翻訳する必要がある。 Sample のコードこんなふう にした。

type PagefindUI =
[<Emit "new $0($1, $2)">]
abstract Create: obj -> unit

[<Global>]
let PagefindUI: PagefindUI = jsNative

window.addEventListener (
"DOMContentLoaded",
(fun _ ->
let elm = document.querySelector "#search"

if isNull elm then
()
else
PagefindUI.Create(
!!{| element = "#search"
baseUrl = "/blog-fable/"
pageSize = 5
translations =
!!{| placeholder = "Search"
clear_search = "Clear"
load_more = "More"
search_label = ""
zero_results = "\"[SEARCH_TERM]\" now found."
many_results = "\"[SEARCH_TERM]\" ([COUNT])"
one_result = "\"[SEARCH_TERM]\" ([COUNT])"
searching = "Searching \"[SEARCH_TERM]\"..." |} |}
))
)

new PagefindUIpagefind-ui.js を読み込んでいれば global に展開されているので、 Fable の Emit で呼び出すのが多分いちばん楽。 new PagefindUI の引数は本来なら型をちゃんと書いた方が良いが、どのオプションを使うべきか定かでなかったのもあり横着して匿名レコードをぶち込んでいる。 あと pagefind UI の埋め込み対象の要素は Archives ページにだけ作るようにしたので、その要素がない場合のエラー避けをした(これも横着)。

pagefind UI のスタイル調整する

最後に pagefind UI のスタイルを調整する。 CSS custom parameter が提供されているので基本それでスタイル調整する。 light と dark の 2 テーマあるのでここここにで CSS custom parameter を上書きしている。 当然、 pagefind-ui.css の読み込み後じゃないと上書きできないので、読み込み順も注意する。

ただ CSS custom parameter でできることが限定的で、あまり納得行く出来にならない。 なので必要に応じて独自に CSS を書くのが妥当だろう。いまはまだやってないけど。 検索結果の highlight が mark 要素で囲まれたり、検索フィールドにフォーカスすると枠が太くなるのとか、デザインの調和が取れてないと個人的に感じる点は今後いじっておきたい。

その他

あとこれ以外にも blog-fable 固有の問題があった。 それは開発サーバとして使ってる Suave がデフォルトで未知の拡張子のファイルを配信できず 404 になるというものだ。 一度知ってしまったら簡単だけど、これは結構調べるのに時間がかかった。

デフォの MIME type → suave/src/Suave/Combinators.fs

拡張はこうやる。 suave/examples/Example/Program.fs 拡張した MIME type の設定を Suave の startWebServer に渡したらいい。

pagefind は生成した Wasm に付与した 4 つ独自の拡張子を持っているので、それらを application/octet-stream で配信するものだと明示してやらないといけない。 こんなふうにした。

let cfg =
{ defaultConfig with
homeFolder = Some(home)
compressedFilesFolder = Some(home)
bindings = [ HttpBinding.create HTTP IPAddress.Loopback port ]
listenTimeout = TimeSpan.FromMilliseconds 3000.
mimeTypesMap =
Writers.defaultMimeTypesMap
// NOTE: Add custom mime types for pagefind to prevent 404 error.
@@ ((function
| ".pagefind"
| ".pf_fragment"
| ".pf_index"
| ".pf_meta" -> Writers.createMimeType "application/octet-stream" false
| _ -> None)) }

(ちなみに Suave のページが不安定みたい)


これで pagefind の設定は第 1 回戦終了って感じ。 まだ気に入らない点あるし検索の使い勝手も検証してないけど、ないよりいいやろ。 ちょくちょく改善入れていきたい。