ブログをZolaに移行した

  • 12 分の読了時間
  • タグ: 
  • blog
  • Zola

ブログを Zola に移行した。
今まではHugoとか、Docusaurusとか使ってみたものの、なんかいまいちで…。
何が気に入らなかったかというと、機能性と速度。

機能でいうと、検索ができなかったり、Disqus に対応していなかったり…。
テーマによっては対応しているものもあるんだけど、今度はビルドが遅くてプレビューがめんどくさくなってしまったり。

以前から Rust 製の Zola が気になってチェックしていたものの、テーマが少なくて移行する気にはなれなかった。
最近になって、「いいテーマないかなぁ〜」とテーマを眺めていたらよさそうなテーマを発見!

DeepThought
https://www.getzola.org/themes/deepthought/

ローカルで確認してみたところ、結構しっくり来たのでブログの移行を検討して、今に至る。

Zola をインストール

まずはzolaをインストール。

brew install zola

あとで発覚するんだけど、brewでインストールしたzolaは検索が使えない(小文字zolaはコマンドを指す)。

記事を投入

今まで書いていた記事 (Markdown) をcontentフォルダにコピー。
_index.mdも作成する。

テーマをカスタマイズ

トップページをブログ一覧に変更

このテーマ、トップページがHomeになっているので、トップページにブログ記事の一覧を表示するようにカスタマイズする。

テンプレートにあるsection.htmlindex.htmlとして使うことで、ブログ記事の一覧表示を実現する。

cp ./themes/DeepThought/templates/section.html ./templates/index.html

あとはzola serveで動作確認。
実際に確認してみるといろいろと気になることが…。

  • read moreが動作してない
  • パスが今までと異なる
  • カテゴリタグが機能していない

read more

read more は<!-- more -->のようにmoreの前後に空白が必要。

URL のパス修正

他のSSGでは、frontmatterslugを定義して URL としていると思うが、Zolaはディレクトリ構成がそのまま URL になるらしく、_index.mdを各所に作らなければならないようだったので、contentディレクトリにすべて保存している(うまいこと_index.mdを継承する方法があるのかもしれないが、知らない)。
この方法を採ると、今までと URL が変わってしまうので困る。
そこで、URL はfrontmatterpathで指定する。

この記事の場合はこんな指定。

path = "2022/06/03/moved-blog-to-zola"

パスの先頭に/は書いてはいけない。

参考ページ
https://www.getzola.org/documentation/content/page/

カテゴリとタグの書き方

Zola はカテゴリとタグの書き方が他の SSG と違った。

[taxonomies]
categories = ["blog"]
tags = ["blog", "Zola"]

frontmatter がYAMLの場合、hugo convertを使うと一気にTOMLに変換できる。
https://gohugo.io/commands/hugo_convert_totoml/

複数行を置換する場合、Visual Studio CodeBBEditあたりが使いやすい。

参考ページ
https://zenn.dev/anz/scraps/ebf857a5cbcfb6

日本語検索

Zola は検索に対応しているんだけど、標準では日本語検索に対応していないので、日本語検索に対応させるのは少々面倒。

  • zolaは日本語の辞書を持っていない
    • 日本語辞書を含むzolaを作らなくてはならない
  • zola build で作れるインデックスはdefault_languageの1種類のみ
    • 日本語検索はできるけど、英単語検索はできない…という状況に陥る
  • 読み込めるインデックスは1ファイルのみ
    • 日本語インデックスと英単語インデックスを作ってもそのままでは両方読み込まない

これらを解決しないと普通に検索できないということ。
上記の解決策を下に書いていく。

日本語辞書を持つ Zola を作成

日本語検索に対応する方法はオフィシャルサイトに書いてある。

Note: By default, Chinese and Japanese search indexing is not included. You can include the support by building zola using cargo build --features search/indexing-ja --features search/indexing-zh. Please also note that, enabling Chinese indexing will increase the binary size by approximately 5 MB while enabling Japanese indexing will increase the binary size by approximately 70 MB due to the incredibly large dictionaries.

https://www.getzola.org/documentation/content/multilingual/

日本語検索に対応するzolaを作成するコマンドは次のとおり。

cargo build --features search/indexing-ja --release

ビルド自体はたいしたことなくて、上記コマンドで解決する。
ただし、Rust のビルドは遅いと言われているように、Zola のビルドも遅い!
一回ビルドするだけならいいんだけど、記事を公開する度に毎回ビルドする場合はきつい。

日本語検索に対応したときのzola自体のファイルサイズはこちらの記事をどうぞ。
https://www.jpgov.art/posts/zola-binary-columes-with-ja-zh-support/

検索時に必要なファイルをダウンロード

不足しているのは下の3ファイル。

これらをダウンロードしてstaticに保存。

一部、修正が必要なところがある。

diff --git a/static/lunr.jp.js b/static/lunr.jp.js
@@ -96,7 +95,10 @@
       }
     })();

-    lunr.Pipeline.registerFunction(lunr.jp.stemmer, 'stemmer-jp');
+    lunr.Pipeline.registerFunction(lunr.jp.stemmer, 'stemmer-ja');
+    lunr.jp['wordCharacters'] = '一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Za-zA-Z0-90-9';
+    lunr.jp.trimmer = lunr.trimmerSupport.generateTrimmer(lunr.jp.wordCharacters);
+    lunr.Pipeline.registerFunction(lunr.jp.trimmer, 'trimmer-ja');

stemmer-jpjpjaの違いがある(paが違う)ので要注意。

英語と日本語の検索結果をマージ

テーマの検索ロジックを、日本語と英語の複数インデックスに対応させる。
テーマで使っているsite.jsを持ってきて編集する。

cp ./themes/DeepThought/static/js/site.js ./static/js

日本語インデックス (searchIndex) の他に、英単語インデックス (searchIndexEn) も読み込んでおく。
両者の結果をマージして検索結果とする仕組み。
searchIndexsearchIndexEnzola buildで作成するので、後述。

diff --git a/static/js/site.js b/static/js/site.js
@@ -139,12 +139,13 @@ function search() {
   };
   var currentTerm = "";
   var index = elasticlunr.Index.load(window.searchIndex);
+  var indexEn = elasticlunr.Index.load(window.searchIndexEn);

   $searchInput.addEventListener(
     "keyup",
     debounce(function () {
       var term = $searchInput.value.trim();
-      if (term === currentTerm || !index) {
+      if (term === currentTerm || !index || !indexEn) {
         return;
       }
       $searchResults.style.display = term === "" ? "none" : "block";
@@ -153,7 +154,9 @@ function search() {
         return;
       }

-      var results = index.search(term, options);
+      var resultsJa = index.search(term, options);
+      var resultsEn = indexEn.search(term, options);
+      var results = resultsJa.concat(resultsEn);
       if (results.length === 0) {
         $searchResults.style.display = "none";
         return;

参考サイト
https://qiita.com/dalance/items/0a435d66e29f505faf6b

ビルド

ビルドする際の注意点がある。

  • zolaは日本語インデックス対応版を使う
  • ビルド時に英語インデックス日本語インデックスの2つを作る

2つのインデックスを作成

英語と日本語のインデックスを作る方法は単純で、ビルドを2回する。
default_languagejaをベースにしたいので、初めに英語インデックスだけ作る都合上、config.tomldefault_languageenにしておく。
それと、日本語インデックスの作成に結構時間がかかるので、ローカルではenのままzola serveで確認した方が効率がいいという理由もある。

# default_language = "en" で 英語インデックス作成
zola build
cp ./public/search_index.en.js .

# default_language = "ja" で 日本語インデックス作成
sed -i -e 's/^default_language = "en"/default_language = "ja"/' config.toml
./zola build

# 英語インデックスの変数名を変更し、publicにコピー
sed -i -e 's/^window.searchIndex =/window.searchIndexEn =/' ./search_index.en.js
cp ./search_index.en.js ./public

これで、下表の状態になる。

インデックスファイル名変数名
英語search_index.en.jssearchIndexEn
日本語search_index.ja.jssearchIndex

この表の内容はsite.jsで参照しているので、先述のsite.jsを参照。

ここまでで検索は解決。

公開

今回はCloudflare Pagesを使ってみることにした。
GitHub のpostsブランチに push で GitHub Actions を使って build & publish をする。

ここで困ったのは 日本語インデックス対応版zola のビルド時間。
初めはキャッシュで解決しようと actions/checkout@v3 を使ってみたんだけど、Zola をsubmoduleにしていたせいでキャッシュが効かず。

次に日本語インデックス対応版zolaartifactとして保存しておき、それを活用しようと考え、 download-artifact を使ってみたんだけど、ビルドしたzolaをうまくダウンロードできず。

最終的にはdownload-workflow-artifactで解決した。

Zola をコンパイルすると公開までの時間は16分くらいかかる。
artifact に置いといたzolaを使うと3分くらいで済む。

Cloudflare Pages のプレビュー機能

Cloudflare Pagesmainブランチ以外だとプレビューとして扱い、サブドメインで専用ページを用意してくれる。
公開する場合はmainブランチを使うことになるということ。

今回はcloudflare/wrangler-action@2.0.0を使った。

コメント

ある程度動くようになったものの、デプロイしてみるとコメントが表示されないことに気付いた。
コメントはDisqusを使ったんだけど、うまく動作しなかったようだ。

Disqus にログインして状況を確認してみると、なぜかトップページの URL でレコードが作成されていた。
明らかに実装側の問題だろう…ということで、ソースをコピーして、少々修正。

cp ./themes/DeepThought/templates/page.html ./templates
diff --git a/templates/page.html b/templates/page.html
--- a/templates/page.html
+++ b/templates/page.html
@@ -184,8 +184,8 @@
 {% if page.extra.comments and config.extra.commenting.disqus %}
 <script>
   var disqus_config = function () {
-    this.page.url = "{{config.base_url | safe}}";
-    this.page.identifier = "{{ current_path | safe}}";
+    this.page.url = "{{current_url | safe}}";
+    this.page.identifier = "{{current_url | safe}}";
   };

   (function () {

各ページ (Markdown) のfrontmatterにコメントを有効にする記述が必要。

[extra]
comments = true

これでコメントも今まで通りになった。

課題

カテゴリとタグの URL がおかしい。
たとえば、こんな感じ。

名称URL のパス
ブログburogu
ben

これを解決するにはfrontmatterにこのように書けばいいらしい。

tags = [{name = "ブログ", slug = "blog"}, {name = "", slug = "book"}]

でも、これをやるのは面倒なのでしばらく放置。

参考ページ
https://zenn.dev/anz/scraps/ebf857a5cbcfb6

まとめ

ディレクトリ構成

blog
├── config.toml
├── content
│   ├── ︙
│   ├── 2022-06-03T2248-ブログをZolaに移行した.md
│   └── _index.md
├── static
│   ├── js
│   │   └── site.js
│   ├── lunr.jp.js
│   ├── lunr.stemmer.support.js
│   └── tinyseg.js
├── templates
│   ├── base.html
│   ├── index.html
│   └── page.html
└── themes
     └── DeepThought

config.toml の主要部分

base_url = "https://blog.teapla.net"
build_search_index = true

default_language = "en"
theme = "DeepThought"
taxonomies = [
	{name = "categories", feed = true, lang = "ja"},
	{name = "categories", feed = true, lang = "en"},
	{name = "tags", feed = true, lang = "ja"},
	{name = "tags", feed = true, lang = "en"},
]

navbar_items = [
	{ code = "ja", nav_items = [
		{ url = "$BASE_URL/", name = "Blog" },
		{ url = "$BASE_URL/tags", name = "Tags" },
		{ url = "$BASE_URL/categories", name = "Categories" },
	]},
	{ code = "en", nav_items = [
		{ url = "$BASE_URL/", name = "Blog" },
		{ url = "$BASE_URL/tags", name = "Tags" },
		{ url = "$BASE_URL/categories", name = "Categories" },
	]},
]

[extra.commenting]
disqus = "${DISQUS_SHORTNAME}"

GitHub Actions のワークフロー

Zola をビルドしてartifactに置くワークフロー

name: Upload release zola
on:
    workflow_dispatch:

env:
    ZOLA_TAG: v0.15.3

jobs:
    build:
        name: Upload Zola binary
        runs-on: ubuntu-latest
        steps:
            - name: Build Zola
              run: |
                  git clone https://github.com/getzola/zola.git -b ${{ env.ZOLA_TAG }} --depth 1
                  cd zola
                  cargo build --release --features search/indexing-ja
            - name: Create Release
              uses: actions/upload-artifact@v2
              with:
                  name: zola-${{ env.ZOLA_TAG }}
                  path: ./zola/target/release/zola

記事を push したときに動くワークフロー

途中でしていている${RUN_ID}は、日本語インデックス対応zolaを作成したワークフロー実行時の ID。
artifact の URL から${RUN_ID}を控えておく必要があるし、zolaのバージョンを上げたら${RUN_ID}を変える必要がある。

name: Publish to Cloudflare Pages
"on":
    workflow_dispatch:
    push:
        branches:
            - main

env:
    ZOLA_TAG: v0.15.3

jobs:
    publish:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3
            - name: Checkout theme
              run: |
                  mkdir -p themes/DeepThought
                  cd themes/DeepThought
                  git init
                  git remote add origin https://github.com/RatanShreshtha/DeepThought.git
                  git fetch --depth 1 origin 8bf64262791f968dcc3e72d82195f9ebb1a39a69
                  git checkout FETCH_HEAD
                  rm -fr ./content
                  rm -fr ./static
            - name: Download zola-${{ env.ZOLA_TAG }}
              uses: dawidd6/action-download-artifact@v2
              with:
                  github_token: ${{ secrets.UPLOAD_PAT }}
                  workflow: upload-zola.yml
                  workflow_conclusion: success
                  run_id: ${RUN_ID}
                  name: zola-${{ env.ZOLA_TAG }}
            - name: Make search_index.en.js
              run: |
                  chmod u+x ./zola
                  ./zola build
                  cp ./public/search_index.en.js .
            - name: Build pages
              run: |
                  sed -i -e 's/^default_language = "en"/default_language = "ja"/' config.toml
                  ./zola build
                  sed -i -e 's/^window.searchIndex =/window.searchIndexEn =/' ./search_index.en.js
                  cp ./search_index.en.js ./public
            - name: Publish
              uses: cloudflare/wrangler-action@2.0.0
              with:
                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
                  command: pages publish public --project-name=${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME }}