ブログをZolaに移行した

Ken published on
15 min, 2891 words

Categories: blog

ブログを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 }}