個人ページ, ブログを移行した

旧個人ページ及び, 旧ブログを本ウェブサイト (roki.dev), 本ブログ (roki.dev/roki.log, roki.dev/roki.diary) に移行した. 以下では, 移行した経緯や技術的概要, 本サイトおよびブログの方針について (ゆるく) 紹介したい.

移行に至った経緯

移行前の旧ブログ1の構成では, static site generator である pelican を使っていた. 以下に, それを使ってそこそこの期間の運用をした上での実状, 感想を挙げる.

  • テンプレートプラグインが充実しており, 設定も非常に少ない記述から簡潔に行えるようになっていて, 主観的な感想として pelican は使い勝手の良いツールであった. 自分の場合は, nikhil-theme を元に拡張して利用しており, いくつかの機能の追加実装や bug fix, 依存関係の更新作業などを行っていた
  • 記事の執筆は Markdown で行い, D3.js や emscripten を導入していたので, 記事内で簡単なシミュレータや計算の視覚化などでヌルヌル動かしたり, 遊んだりできるようにしていた
  • 執筆時には MathJax が意図通り描画できているか等で瞬時にプレビューを見たかったため, 記事内の更新に合わせて記事のリビルド, ブラウザの自動リロードがされるスクリプトを作り, それを実用していたのでブログ記事執筆時のストレスもそこまではなかった
  • 全体のブログ記事管理の仕組みとして, draft から release ブランチに merge & push すると, Bitbucket Pipelines が走り2, ブログのビルドと GitHub pages へのデプロイが実行されるようにしていたので, 管理コストや記事公開のための作業も少なく, その点は快適であった
  • その他の細かい作業 (旧ブログ記事) を行うことで“色々とそれなりに”便利にしていた

…とこれまでを振り返ると, あまり不満はなかったのではないかというような気がしてくるが, 全く不満がなかったかというとやはりそうではない.

  • 元テンプレートの Bootstrap のバージョンが低い
  • MathJax が重い
  • テンプレートが膨大
  • 標準 (プラグイン) の検索機能が日本語に未対応
  • リンクのバリデーション機能がない
  • 無料 GitHub アカウントでもプライベートリポジトリが使えるようになり, Bitbucket と GitHub 間を横断させる必然性がなくなった (GitHub Actions も使えるようになったことで, ブログ管理のすべてを GitHub のみで一元管理できる)
  • 記事や記事内で使うスクリプトを校生するもの (textlint の linter 等) と, ブログそのもの (Jinja2 テンプレート, ビルド, デプロイ, ライブプレビュー, プラグイン管理…) は全くの別物なので, これらの管理を分離したい

無論, 元のブログでも修正, 更新, 機能追加でこれらをすべて満たそうとすることはできるが, もうそこまでするならいっそのことリニューアルしてしまったほうが…🤔となってしまった.

ブログの他にも, 個人 (プロフィール) サイトを公開しており, それにおいては Typescript + React を使って構築していた3. こちらは特に何か変わったこともしていないので, 特筆すべきトピックもないのだが, そこまで DOM 操作をするわけでもないプロフィールページにこれらの技術を用いたのはオーバースペックだったし, bundle.js の重さからしてもあまり理にかなっていなかったように思う.

新個人サイトとブログ

そのようなわけで, 上記のようなモチベーションがあり, 本個人サイトとブログを新設したわけだが, 今回新設したことによって, これらの不満足な点についてすべて解消ができたと考えている. それを果たすことのできた要因や特徴について, いくつか挙げていければと思う.

Hakyll について

今回, この新個人サイトとブログを新設するにおいては, Haskell 製の static site generator である Hakyll を使った. 一言に static site generator と聞くと, テンプレートがいくつかあって, それのうちの何かを選び, 場合によってはカスタムなどして, また config があって, そこに任意のほげほげを設定して…というイメージが湧くかもしれない. Hakyll においても, 勿論そのような使い方が可能なのだが, Hakyll はあくまでも static site generator そのものを作るためのライブラリであるという点で特徴的である (と私は感じている). これにより, static site の細かい部分にまで手を付けることができるのである.

例えば, 上記で問題視していた「MathJax が重い」についてであるが, これについてはまず KaTeX\KaTeX へ移行することで対応を行おうと当初考えていた. しかしよくよく考えてみると, unixFilter といったような, 外部のプログラムを Compiler として扱うための便利な関数もあるので, こういったものを使って KaTeX\KaTeX から吐き出される数式のタグをサイト生成時に埋め込めれば, わざわざ javascript から動的に書き換える必要すらないのでは…となり4, 今回はそのように実装することで, 静的な数式レンダリングが行えるようになった5.

ただこれについては, ビルド時間が比較的長くなるという問題がある. 後述しているように, このウェブサイトのビルド, デプロイに関してはすべて GitHub Actions 上で行っているのもあり, デプロイ前のビルドで時間がかかっても, (GitHub Actions 上で許される範囲内ならば) 特別問題はないのだが, 手元で記事のプレビューをすぐしたいとき等にこれはストレスになる (Hakyll はデフォルト (hakyll, hakyllWith 等) で watch オプションが使えるようになっている. これは, 記事等の更新があった場合に自動でビルドを再実行してくれるものである). 今回は hakllWithArgs を用いて, まずこちらでプログラムへの引数を拡張し, それによって static site のビルドの挙動を切り替えられるようにすることでこの問題に対処した. 具体的には, 以下の --preview フラグをセットして実行することにより, ビルド時に KaTeX\KaTeX のレンダリングを行わないようにし, 生成される HTML の head の中に KaTeX\KaTeX を動的にレンダリングする js ファイルを埋め込むようにした.

別の似たような事例として, 不必要な js ファイルの読み込みを行わないようにするといった工夫ができる. このブログでは d3.jsmath.js を使えるようにしてあるのだが, 全ての記事でこれらを利用するわけではないので, そのようなときはスクリプトファイルの読み込みを抑えたい. ある記事でこれらのうちの何かが使われたとき, スクリプトファイルの読み込みが必要になるであろうシーンは, 単一の記事を表示するときと, teaser 内にそれが表示範囲として入っていてかつ記事一覧を表示するときであり, 単一の記事を表示しているときは, 単に metadataField にその読み込みの記述があるかないかで判定できるが, 記事一覧を表示するときはその表示される記事一覧の teaser に表示範囲として含まれているか判定しなければならない. が, teaser 内のコンテンツまで読んで判定するとなると, 後々なにか変わった読み込み方をしたくなったときなどに柔軟な対応ができなくなる等の懸念事項があったので, 今回は記事一覧に表示される記事の metadataField に読み込みの記述があれば (teaser に含まれているかとは無関係に), その記事一覧のページにスクリプト読み込みを埋め込むようにしている.

…というように, 様々なパフォーマンスに対するニーズについて柔軟に対応ができる点も, Hakyll の良いところであると思う.

リッチな設定ファイル

個人ページの Contributions の一覧は (より多く増やしていきたいという気持ちも込めて) HTML テンプレートに直接埋め込むのではなく, 外部ファイルから読み込むようにしている. その外部ファイルの形式として, 今回は Dhall を採用した. Dhall は簡単に言えば, json に型, 関数, インポートの機能が乗っかった設定ファイル言語である6. ここでは Dhall そのものについて詳しくは説明しないが, 例えば, 以下のように各ジャンルについて Union で定義し, 文字列への射を定義することで, 誤った文字列の設定を静的に防ぐといったことができる.

Haskell との親和性も高く, 例えば Dhall.input 等で簡単に読み込むことができる.

RSS/Atom Feed や Site map

このサイトは, トップの個人ページの下に二つのブログがぶら下がっている構造をしており, よって, Site map や Atom に関してそれぞれのブログから提供する必要がある.

これは, 標準の renderRSSrenderAtom では対応できないのだが, renderRssWithTemplatesrenderAtomWithTemplates で独自の XML テンプレートやコンテキストを渡すことができるので, サイトの構造に合わせて柔軟な対応ができる. Site map については標準の機能として盛り込まれていないのだが, Hakyll のサイトからも紹介されているこのブログ記事の通り, Feed の生成と同様, 以下のように XML 用のテンプレートファイルを読ませたり (Lucid 等の) DSL で生成したもの等を使えば良い.

バージョン情報の埋め込み

このブログ及びウェブページを生成するアプリケーションのバージョン情報には Cabal ファイルで定義されたパッケージバージョンと Git のコミットハッシュ値を埋め込んでいる.

$ stack exec site -- --version
The static site roki.dev compiler
version: 0.1.0.0, commit hash: d4dcc402eb6ac271ec070a539e206580ad9cbe5e

Git のコミットハッシュ値を埋め込むのには Development.GitRev.gitHash が非常に便利で役立った.

CSS フレームワーク Bulma

このブログは CSS のフレームワークとして Bulma を使用している. 当初は以前のブログと同様に Bootstrap を使おうかと考えていたが, 極力 javascript をなくし, 最小構成を軽くしたいと考えていたため, こちらを採用した. ウェブサイトのデザインについて疎い私であっても, 一応このような“それなりにそれっぽい”見た目を構成できたという点において, この CSS フレームワークには助けられたといえる. 個人的に非常に便利だったのが, Bulma の提供している Helpers で, これは単に色の定数値や margin, padding 等に関するショートカットが用意されているのだが, 「ほんのちょっと手を入れたい」というときのタイプ数がかなり抑えられるので, 精神的負担が少なく, とても良いものであった.

また, 数学系の記事を書く際には「定義」, 「命題」, … といったような見出しをつけたいものだが, このスタイルの作成についても助けられた (Bulma が便利というのもあるが, 単純に SCSS の展開能力にも助けられた).

これを使って, 例えば以下のように書けば

<div class="m-prop">
<header class="m-prop-title"><p>ABC 予想</p></header>
<div class="m-prop-content">
\\[ a + b = c \\]
を満たす, 互いに素な自然数の組 \\(a, b, c\\) に対し, 積 \\(abc\\) の互いに異なる素因数の積を \\(d\\) とおく.
このとき, 任意の \\(\epsilon\gt 0\\) に対して,
\\[ c\gt d^{1+\epsilon} \\]
を満たす組 \\((a,b,c)\\) は高々有限個しか存在しない
</div>
</div>

次のような表示になる.

ABC 予想

a+b=c a + b = c を満たす, 互いに素な自然数の組 a,b,ca, b, c に対し, 積 abcabc の互いに異なる素因数の積を dd とおく. このとき, 任意の ϵ>0\epsilon\gt 0 に対して, c>d1+ϵ c\gt d^{1+\epsilon} を満たす組 (a,b,c)(a,b,c) は高々有限個しか存在しない

CI/CD と全体の管理体系

前述したとおり, このブログ, ウェブページは GitHub Actions でビルド, デプロイを行っており, それ以外の CI/CD としていくつかボットを設定している. この構成の特徴としては, ドラフトをリモートリポジトリに非公開の状態で保存できるようにしてあること, またそれが Git 上で矛盾しないように構成している点が挙げられると思う. といっても, 下図の通り非常にシンプルな構成である.

個人ページ/ブログ管理の構造

執筆時点現在では, 少なくとも GitHub 上において「プライベートブランチ」なる概念は存在しない. 従って, 非公開情報を扱いたいのならば, 必然的にそれをプライベートリポジトリとして扱う必要があるが, この構成はドラフトとリリースでリポジトリの公開情報をそれぞれわけることができるということに加えて, ブログ記事そのものに対する管理と, ウェブサイトを生成するアプリケーションの管理を分離することができるという利点がある. 例えば, ブログ記事の管理のほうで扱われるのは Markdown テキスト, 画像などのメディアファイル, 記事内で使うような js ファイル等であり, これらの linter やメディアファイルへの自動圧縮などの CI/CD を構成する際に, アプリケーションの CI と完全に分離できるのである.

なお, この構成をするのには既存の Action である GitHub Actions for GitHub Pages が非常に役立った. 特に, 既存のファイルを残すように設定できたり, 外部のプロジェクトへ Push できる設定項目については特筆すべき内容で, この Action のおかげで全体の構成をスムーズに行うことが出来たといっても過言ではない.

次に, パフォーマンス計測についてであるが, 一応デプロイした後に, Google PageSpeed Insights で, デプロイ後の状態を計測させるようにしている. 現状, PC 版は 90, モバイル版は 65 を閾値として取り敢えず合否を設定しているが, モバイル版のスコアをもう少し上げられるようにしていきたいところである. これには, actions/github-script が非常に役立った. javascript を直接書き込める Action で, 手軽に導入が勧められる. 以下の処理においては, GitHub pages の状態がデプロイ完了になるまでポーリングさせている.

さて, ここまで見てみて, もしかすると, どうやって roki-web と roki-web-post 側で同期を取っているのか, 普段はこの環境でどのようにブログの更新等行うのか等, 疑問に思うかもしれないので少し補足しておく. まず, ブログ記事の更新は, すべて roki-web-post リポジトリ内で行う. roki-web リポジトリ内では, ブログ記事については一切触らない. これをもし触ってしまうと, GitHub Actions によって roki-web の master ブランチにプッシュした際にコンフリクトすることになるので, この点では注意が必要である (が, まあもし間違えて触ってしまったとしてもそこは Git なので別になんとかなる). 反対に, ウェブサイトを生成するアプリケーションについて, roki-web-post では一切触らない. ウェブサイトを生成するアプリケーションについて何らかの変更を加えたい場合は, roki-web のみから行うようにする.
次に roki-web で何らかの変更が加えられたときに, roki-web-post がどのようにそれを取り込めばよいかであるが, これは単純に remote を複数登録しておいて, 場合によって切り替えて pull してくれば良い. もっと言えば, 下記のように設定しているので,

$ git remote -v
origin  git@github.com:falgon/roki-web-post.git (fetch)
origin  git@github.com:falgon/roki-web-post.git (push)
site-system git@github.com:falgon/roki-web.git (fetch)
site-system git@github.com:falgon/roki-web.git (push)

roki-web の変更を取り込むときは単に

$ git pull site-system master

等をすれば良いようにしている. このとき, --set-upstream 等でデフォルトを origin に設定しておくと, ぼーっとしていて site-system 取り込んでしまった!だとか site-system に push してしまった!等という事故を防ぎやすくなる.

なお, roki-web へのデプロイは deployment key を使って行っているが, これは Action 内でデフォルトで使える GITHUB_TOKEN では, あるワークフローによって別のワークフローをトリガーできないためである.

When you use the repository’s GITHUB_TOKEN to perform tasks on behalf of the GitHub Actions app, events triggered by the GITHUB_TOKEN will not create a new workflow run.

この回避策に関しては, create pull request という GitHub Actions のドキュメント中に記載があるので, 必要ならば参照すると良いかもしれない.

PR に対するプレビュー

依存パッケージのバージョン更新や追跡等を Bot に管理させると管理コストを抑えることが出来る. このブログも依存パッケージのバージョン更新, 追跡は Dependabot を利用することで行っているが, こういった PR を自動で発行してくれるような Bot を運用する上では, 人間はもはやマージボタンをただ押せば良いだけ, という環境を極力整備したいものである. しかし, 今回のようなウェブサイトのプロジェクトの場合, テスト実行のみではどうしても保守ができない箇所が発生するし, マージする前に念の為一度実際にプレビューを見ておきたいといった理由で, Dependabot から投げられてきた PR をただそのままマージするといったことが出来ない場合がある. 手元にプルしてきて毎度確かめればそれは勿論プレビューができるわけだが, 前述したように, こちらは出来る限りボタンをぽちぽちするだけで完結したい.
そこで, 今回は CirCle CI の Artifacts を用いて, PR (のコミット) 毎にプレビューを閲覧できるようにした. CircleCI の Artifacts は (本エントリ執筆時点において) GitHub Actions と異なり, Artifacts に対するブラウジングが可能となっており, 従って, そこに HTML ファイルを配置すれば, ビルド毎の一時的なウェブサイトが確認できるようになる7.

PR とプレビュー生成の概観

PR に対するプレビュー生成の概観は上図のようになっている. まず GitHub Actions が PR をトリガーに内部でウェブサイトを生成し, Google Drive にアップロードする. ここで, アップロードする tarball にはプレフィックスとしてコミットハッシュ値をつけておく. その後 GitHub Actions が CircleCI の job を起動する. このとき, コミットハッシュ値をパラメータとして付加して起動する. CircleCI は与えられたコミットハッシュ値を参考に Google Drive から Artifacts tarball をダウンロード, Google Drive から削除し, CircleCI 上の Artifacts に展開する. そして, 最後に Bot がその job のログが見れる URL と展開された Artifacts の index.html の URL を該当 PR にコメントする.
以上により, 手元でプルしてビルドして…といったプレビュー作業を自分で行うことなく, 一時的なプレビューサイトを事前に確認することができるようになっている.

さて, このようにいくつかのサービスを横断させているのにはそれなりの理由がある. まず第一に, Artifacts としてアップロードするウェブサイトを CircleCI 上ではビルドしないように済ませる必要があった, ということ. CircleCI は 1 job につきデフォルトで 4GB の RAM が割り当てられるが, このウェブサイトをビルドする際に使われる Cabal という Hakyll の依存パッケージをビルドすると CircleCI 上ではどうしてもメモリリソース不足となってしまい, ビルドができなかった. -j1 で事前ビルドしたりといった回避策も試行してみたものの, 私の場合では虚しく, メモリリソース不足となってしまった.

よって, CircleCI 上でのビルドは断念せざるを得なかった, というのと, GitHub Actions 上でそもそもビルドを行っていたので, そこでビルドした tarball をそのまま利用出来た方が無駄がないという面もあり, わざわざ CircleCI 上でビルドを行う必然性も特になかったので, 今回は Artifacts のみを利用することで対応している.

第二に, ワークフローの実行中に Artifacts URL を取得することが不可能であったということ. GitHub Actions の Artifacts の URL を CircleCI に渡してダウンロードさせることが出来れば理想的だが, この URL の取得は GitHub Actions の仕様上できないので, ワークフローの内部でビルドした成果物を GitHub Actions Artifacts にアップロードするのではなく, Google Drive といった外部のストレージサービスへアップロードすることで対応を行わざるを得なかった.

総括

ここまで振り返ってみて, 割とまだまだ拘れる点はあると感じている. 例えば, テンプレートの Lucid 化だとか, 現在の実装をもう少しモナドの合成等ですっきり表せないか等である. 一応, このブログ, ウェブページを構築した際に何をやったのかまとめやすいように, 構築するのに行った大体のタスクについてラベルをつけておいたので, 気になる場合は見てみても良いかもしれない. そのようなわけで, 今後はこのブログ, ウェブサイトを使って以前と同様何かしら書いていければと思っているので, (お手柔らかに) よろしくお願い致します.

追記 1

  1. 利用している CI/CD, Bot は様々なものを使わせて頂いているが, そのうちの 1 つとして, フォーマッタ等でコードスタイルを判定, 整形して PR を投げてくれる restyled.io という Bot が挙げられる. 見てみたところ, Dhall は対応していなかったので, 今回は PR を投げて Dhall の対応を追加してみた. エディタの保存時に自動でフォーマッタを動くようにしていればそもそも不要ではあるが, たまにそのような機能を切ったりすることがあり, ついそのまま push してしまうなんということがあるので, そういった際にこの Bot があると, 一手間作業を減らすことができる
  2. KLablog にてこちらの内容を紹介する記事を執筆し, 公開頂きました

追記 2 (2020/11/19)

GitHub Actions の cron を利用することで, 簡単な予約投稿機能を実現した. 理想としては, GitHub Actions 上で at コマンドのようなものが使えれば最適であったが, 残念ながらそのようなものはないので, 予約投稿したい内容の差分が含まれたブランチを用意しておき, 該当時刻の cron とデプロイ処理が記述された GitHub Actions の yaml ファイルをプッシュするという少し強引な方法で実現した. ただ, この yaml ファイルを一々手書きしていては堪えるので, (Hakyll のテンプレート機能を使って) 入力から自動生成する簡単なツールを作ることで対応した.

$ stack exec spa -- --help
Usage: spa [--version] COMMAND [-d|--date date] [-b|--branch-name ARG] [-y]
  The roki-web Scheduling Post Action manager 0.1.0.0

Available options:
  -h,--help                Show this help text
  --version                Show spa version information
  -d,--date date           Date to schedule (mm-dd-%H:%M)
  -b,--branch-name ARG     The name of the branch you plan to deploy
  -y                       Generate a file without checking the branch name and
                           repository name

Available commands:
  cexpr                    show crontab expression
  yaml                     generate GitHub Actions yaml from template
  clean                    clean up and remove cache

以下のように生成できる.

$ stack exec spa -- yaml -d $(date "+%m-%d-%R") -b my-awesome-scheduled-post # 現在時刻で生成 (つまり来年に実行される)
current branch name is: draft
Are you sure you want to continue connecting? (y/N)y
Initialising...
  Creating store...
  Creating provider...
  Running rules...
Checking for out-of-date items
Compiling
  updated tools/scheduled_post/template.yml
  updated my-awesome-scheduled-post.yml
Success

後は, この yaml ファイルを roki-web-post のメインブランチ内の .github/workflows/ 配下に置けばよい. GitHub Actions における cron のタイムゾーンは UTC なので, cexpr という JST での時刻入力を POSIX cron 形式の UTC 時刻で出力するコマンドを念の為用意している.

$ stack exec spa -- cexpr -d $(date "+%m-%d-%R")
00 15 11 09 *

予約投稿の実施後には, 上記で生成した yaml ファイルがメインブランチ内に残ることとなり, これをそのままにしておくと, 一年後にまた実行されてしまう. そこでそのファイルの削除忘れが起きないよう, 予約投稿が完了次第, メインブランチ内の該当 yaml ファイルを削除する PR が自動的に発行されるようにしている.

予約投稿完了後に自動発行される PR

ここで, PR のマージを忘れてしまうと先に述べたのと同じことになってしまうのだが, かといって, 事前に確認もなく draft ブランチへ変更を加えたくもなかった. 今回は, これらの兼ね合いを考慮した上で, そこそこ納得のいく落とし所がつけられたのではないかと思っている.

追記 3 (2021/06/26)

CircleCI の Artifacts へのアップロードが完了した後に LINE Notify を用いて通知するようにした (#159).

LINE Notify によるウェブサイトビルド完了通知

CircleCI の Environment Variables に API トークンを LINE_NOTIFY_TOKEN としてセットし, 以下のように叩いている.


  1. それよりも前は 2016 年にはてなブログ (Roki のチラ裏) で技術系の記事を書いていた. さらにそれよりも前はアメーバブログで技術系の記事を書いていたが, 随分前にもう消してしまっていた…特別意識しているわけではないが, こう見ると二年周期で移行している気がする… (もう移行はしたくないなぁという気持ち)↩︎

  2. ここで貼っている Pipelines のリンクは, このブログへ移行する前の旧ブログに対する最後の変更コミットのもの. 切ない.↩︎

  3. 元々は何か色々と DOM 操作をするつもりでいたのだが, 結局単なるプロフィールページなのでオーバースペックであった. この旧プロフィールページはすでにクローズしているが, 記録としてなんとなくキャプチャしたものを YouTube にアップロードしておいたので, もし興味があれば.↩︎

  4. 同様の取り組みをされているサイトがいくつかあったため, 大いに参考とさせて頂いた.↩︎

  5. これにより, 例えば比較的多くの数式を使っている「エルガマル暗号」といった記事について, Google PageSpeed Insights で何度か計測した結果, 旧ブログ記事と比較してインタラクティブになるまでの時間が PC で平均 7 倍, モバイルで平均 5 倍高速になった (てきとう調べ)↩︎

  6. あくまで設定のための言語であり, チューリング完全ではない. Dhall については, 別途なにか記事を書きたい…↩︎

  7. GitHub Actions にブラウジング Artifacts 機能が追加される予定があるかに関して Support Community にも話題が挙がっている.↩︎