| @ブログ

普段は Vim などのテキストエディターで文章を書いていて、ブログの投稿画面にはできあがった内容をコピペするだけだったので、投稿画面の使いやすさやは気にしたことがなかった。画像のドラッグ・アンド・ドロップ・アップロードや、オン・ザ・フライでプレビューをできるようにしたが、テキスト自体の書きやすさを改善しようとはしてこなかった。

大筋はテキストエディターで書いたとしても、最後に推敲したり、推敲していて気が付いたおかしなところの修正は投稿画面で行うことが多い。やっぱり投稿画面の書きやすさは重要だ。特に長大な内容を編集しているときにテキストエリアが狭いととても使いづらい。ある程度大きさがあり、画面内に大量の文字が表示されるテキストエリアが好みだ。

Lokka が開発されていた 10 年以上前は解像度の小さいディスプレイが主流で、 Lokka の管理画面は小さいサイズのウィンドウで閲覧することを想定したテキストエリアのサイズになっている。今日の高解像度ディスプレイで見ると不便なので画面一杯にテキストエリアが広がるようにした。

投稿画面のレイアウトを改善

iPad からも投稿しやすいように投稿画面のレイアウトも修正した。本文のテキストエリアが広がったためスクロールしないと「スラッグ」以降の入力欄にアクセスできなくなったので、ある程度横幅のあるウィンドウのときにはこれらを右側に配置するようにした。

加えて、フォームを書きかけで保存したかどうかがはっきりせず、未保存の内容があるのに保存せずページから離脱してしまうことがあったので、変更点があるときは背景色を変えてわかるようにした。これにより記入内容を保存せずページを離脱してしまい、内容が失われるという悲劇を回避できるようになった。やり方は適当に検索して出てきた Stack Overflow を参考に、ページを読み込んだ時点で JavaScript でフィールド内の要素を JSON.stringify して data attribute に格納し、その後各フィールドの input イベントを監視して変更があるかどうかをチェックしている。

class FormObserver {
  constructor() {
    this.initializeFields();
    this.observeFieldsChange();
  }

  initializeFields() {
    const fields = Array.from(document.querySelectorAll('div.field'));
    for (const field of fields) {
      const inputElement = field.querySelector('input[type="text"], textarea, input[type="datetime-local"], select option:checked');
      if (inputElement === null) {
        continue;
      }
      field.dataset.serialize = JSON.stringify(inputElement.value);
    }
  }

  observeFieldsChange() {
    const fields = Array.from(document.querySelectorAll('div.field'));
    for (const field of fields) {
      const inputElement = field.querySelector('input[type="text"], textarea, input[type="datetime-local"], select');
      if (inputElement === null) {
        continue;
      }
      inputElement.addEventListener('input', () => {
        if (field.dataset.serialize != JSON.stringify(inputElement.value)) {
          inputElement.dataset.changed = 'true';
          field.classList.add('edited');
        } else {
          inputElement.dataset.changed = 'false';
          field.classList.remove('edited');
        }
      })
    }
  }
}

input イベントを監視すると動作がもっさりするのではないかと心配したが、そんなことはないようだ。普通に使えている。

いまのところ管理画面の HTML レンダリングはサーバーサイドで Ruby で行っているが、これ以上凝ったことをやるなら React などを使って JavaScript で HTML を組み立てる方式にしていく方が効率が良さそうだ。

| @技術/プログラミング

偶発的に puma のバージョンを上げたところ Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ISO-8859-1 が多発して厳しい感じになった。

このブログでは puma は v4 系を使っていたが、調べると最近 v6 もリリースされたようで v5 系に上げてみることにした。すると忘れていたのだが puma は v5 系から daemonize する機能が削除され、デーモン化は systemd を使うべしということになっていた。プロセスのデーモン化は puma にやってもらわないと capistrano で deploy するときに面倒なので以前は v5 に上げるのを諦めて v4 を維持していたのだった。

capistrano3-puma が systemd に対応していたのでえいやっと puma を v5 に上げて deploy してみたところ、冒頭の Encoding::CompatibilityError: incompatible character encodings: UTF-8 and ISO-8859-1 が多発してページが全く表示されなくなってしまった。

一方で管理画面やアーカイブページは表示に問題がなかった。どうもファイルの読み込みが発生するページ(このブログではキャッシュを多用していて、ファイルに書き出したキャッシュを読み込んでいる)でエラーが発生しているようだった。

自分で fork した sinatra-cache.gem でファイル読み込みする部分で encoding オプションを指定してみたりしたが問題が直らない。 Haml や Sinatra のバージョンも古いのでこれらも上げてみようかと試みたが、そうするとより盛大にエラーが出てしまう( Haml を v6 にすると html_safe している出力もさらにエスケープされて HTML がぶっ壊れる)。

気になるのはローカル環境( Mac )ではこのエラーが発生しないこと。「これは環境起因では?」と思い至ってガチャガチャやってみたところ修正することができた。

Lokka では Encoding.default_external を参照しつつ String#force_encoding しているところがある。「ひょっとして Encoding.default_external の値がローカルとサーバーで異なるのでは?」試してみたところ、ローカルでは #<Encoding:UTF-8> となる Encoding.default_external の結果が、サーバーでは #<Encoding:ISO-8859-1> となっていた。

以下のブログを参考に、環境変数 RUBYOPT でエンコーディングを指定して puma を動かすことでエラーを回避できた。

systemd 経由で puma を動かすときに環境変数を設定するのは結構難しい。最初は puma が RACK_ENV=production で動かず困ったが、 systemd 用の設定ファイルで EnvironmentFile のパスを指定し、環境変数用のファイルの中で各種環境変数を定義してやる必要があった。こんな感じ。

systemd の設定ファイル

[Unit]
Description=Puma HTTP Server for portalshit (production)
After=network.target

[Service]
Type=simple

WorkingDirectory=/var/www/deploys/portalshit/current
# Support older bundler versions where file descriptors weren't kept
# See https://github.com/rubygems/rubygems/issues/3254
EnvironmentFile=/var/www/app/.config/systemd/user/portalshit_env
ExecStart=/var/www/app/.rbenv/bin/rbenv exec bundle exec --keep-file-descriptors puma -C /var/www/app/portalshit/config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
StandardOutput=append:/var/www/deploys/portalshit/shared/log/puma_access.log
StandardError=append:/var/www/deploys/portalshit/shared/log/puma_error.log

Restart=always
RestartSec=1

SyslogIdentifier=puma

[Install]
WantedBy=default.target

環境変数の定義ファイル

RACK_ENV=production
RUBYOPT=-EUTF-8

puma v5 に移行しようとしている方の参考になれば幸いです。

| @技術/プログラミング

ブログ過去記事の閲覧 UI にはこだわりがある。これまで何度か記事を書いた。

このブログの維持管理で一番時間を割いているのが Archives ページだ。しかしアクセスログを見ると自分以外はほとんど利用していない。完全に自己満なのだが、過去の自分を振り返ることができてとても自分には有意義なページだ。

過去記事を振り返るときには検索をしたくなる。タイトルのみであればページ内検索で探せるが、やっぱり本文込みで検索したい。 Lokka の検索はあるが、検索結果ページは 7 件ずつ(この値はカスタマイズできる)表示で全文表示される。自分は検索キーワードに関する記事が存在するか知りたい訳ではない。著者なのでキーワードに関連する記事があるかないかくらいわかってる。じゃなくて過去の自分がいつ頃どの密度でそのトピックについて書いていたかを知りたいのだ。

タグやカテゴリーで絞り込む手もある。しかしカテゴリーやタグは理想的な分類ではない。二つのカテゴリーを横断するような記事があるし、タグは設定し忘れていることが多い。全文検索が一番頼りになる。

SQL で全文検索的なことをやろうとするとパフォーマンスが良くないだろう。やっぱり全文検索システムが欲しい。

Tantivy と Tantiny

とはいえ、個人のブログで全文検索エンジンを導入するのはしんどい。確かに Apache Solr や Elasticsearch を個人ブログに入れるのはきつい。もっと手軽に使えるものはないか探していて、 Rust 製の全文検索システム Tantivy と、その Ruby クライアントの Tantiny を発見した。

これがめっちゃ簡単で昨日数時間サンデープログラミングをして導入できた。システム環境に適合するビルド済みのバイナリが GitHub にあれば Rust 環境のセットアップすら必要ない。 Gemfile に gem 'tantiny' と書いて bundle install するだけで使えてしまう。

うまくいかなかったもの

最初、同じ Rust 製で Wasm までセットで提供してくれる tinysearch を試した。 JSON 形式で全ファイルを書き出すだけで使えるやつだ。しかし残念なことに日本語では全く使えなかった。自然言語処理をやろうとしていてあるあるのパターンだ。日本語は MeCab などでトークナイズしてやる必要がある。

デフォルトのトークナイザーでもそこそこに優秀な Tantivy

Tantivy にもトークナイザーをカスタマイズできる仕組みはあるが、標準の Simple Tokenizer でもそこそこ精度が高い。固有名詞にちょっと弱いが、辞書ファイルがないので仕方ないだろう。

個人ブログでも全文検索できる時代

このブログは個人ブログだが、画像のリアルタイムリサイズサーバーを動かしている(おかげで S3 の転送量が安くて済んでいる)し、 TF-IDF で関連の高い記事も表示している。それに加えて全文検索まで入れてしまった。こういうのは大手のブログサービスを利用しないと使えない機能だったが、 OSS と新しいプログラミング言語( Go や Rust )のおかげで個人でもそこそこのスペックのサーバーでこれらを利用することができるようになってきた。 MovableType でサイトを構築していた時代から何も進歩していないようで実はとても進歩している。こういう文化の灯火が消えないようにしていきたい。

| @技術/プログラミング

Lokka の検索はキーワード一つにしか対応していなかった。例えば うどん ラーメン と入力すると、確実に うどん ラーメン という語順で検索が行われる仕様になっている。これはちょっと不便だと思ったので半角スペースでキーワードを分割して AND 検索するようにした。つまり確かに うどん ラーメン という語順で文章が書かれていなくても、 ラーメン うどん という語順だったり、そもそも うどんラーメン が離れたところに書いてあるような文章でもオッケーな仕様にした。 diff はこんな感じ。

一般的な検索システムだと入力された検索キーワードを品詞分解したりして半角スペース入れたりせずともいい感じに検索できるのだろうが、データベースから直接検索するシステムではこれくらいできれば十分かなと思ってる。どうせこのブログで検索してるの自分一人くらいだし。

| @雑談

仙酔峡から見る鷲ヶ峰

2020 年の途中( 7 月)から #100DaysToOffload に挑戦し始めた。

本当は挑戦し始めてから一年間で 100 記事かければ良いのだけど、自分の場合はコロナ禍の自粛期間中に結構ブログたくさん書いてたこともあり、 2020 年内に 100 記事書くことを目標にした。 10 月まではいいペースで推移していたが、 11 月に Apple のプライバシー問題の件の翻訳記事を書くので燃え尽きてしまってペースが鈍り、 12 月も調子が上がらず結局 79 記事で止まってしまった。

2020 年の前半は Lokka の ActiveRecord 化も頑張っていて、あと一歩で master ブランチに merge できるところまでこぎ着けたのだが、こちらも途中で燃え尽きてしまった。

自分の手元では ActiveRecord 化が完了していて満足してしまったし、そもそもいまは Lokka を使っている人がほとんどいなくて、誰も望んでいないことに精一杯時間を割くのが虚しく感じられるようになってやる気が萎えてしまった。

燃え尽きは突然やってくる。目標が達成できていないのにほどほどのところで自分自身が満足してしまうとか、自分がやっていることを誰も望んでいないということに気がついたときに、突然やる気が萎えてしまって燃え尽きてしまう。仕事でも 2017 年から 2018 年にかけては結構頑張っていたのだけど、頑張りが成果に結びつかなくて正直燃え尽きてしまった感がある。その後は惰性で何とか今日までやってきているけど、あまり良い状態ではない。

若い頃はモチベーションなんてものは無限に涌いて出てくるものだと思っていたけど、年をとってくると金を積まれても乗り気になれなくてやれないということが出てくることを知った。自分の時間やモチベーションの希少性・有限性を理解し、下手なことに使ってしまって燃え尽きてしまうのは避けなければならない。

新年早々どんよりした記事になってしまった

| @雑談

2020 年、時系列でふりかえるとこんな感じだった。

できたこと

ハワイ旅行

ハワイで最近の SUV に乗って Car Play に感動した。車の買い換えを検討するきっかけになった。ハワイ楽しかったのでまた行きたい。

Akaka Falls Cadillac XT4 Volks Wagen Atlas

ブログの ActiveRecord 化

脱 DataMapper できた。

ブログ UI 刷新

トップページの見た目、アーカイブページにグラフを表示するようにした。ブログ記事を書いてきた実績が可視化されるとやる気が出る。

ウッドデッキの塗り替え

サンドペーパーをかけて塗り直した。

ウッドデッキ塗り直し

庭で焼き鳥

バーベキューに飽きて焼き鳥おじさんになった。

焼き鳥

ハンモック入門

ウキグモ Light & ウンカイ Light 購入。

ハンモック

UL クッカー購入

アルコールストーブ、チタンカップ、メスティン、アルミフライパンなどなど。

アルコールストーブ

山で肉まん・焼売・炊飯

カップラーメンとおにぎりからの卒業。メスティンでの炊飯はめちゃ美味くて感動する。

焼売 メスティンで炊いた米でたまごかけご飯

車の買い換え

スバルインプレッサから Jeep Compass へ。ハワイ旅行でアメ車の SUV に乗って車に対する考え方が変わり、車を買い換えたくなった。国産車はディーラーとの交渉がわずらわしすぎて値引き交渉やオプションがほとんどない外車になってしまった。アメ車はアホみたいにガソリン喰って笑える。プラスチック部品の質が悪い(ゴルフ 2 を思い出す)。

Jeep Compass

ソロで脊振山・金山を縦走

2 年ぶりのソロ縦走。盛夏にソロで 20km 歩いてつら寂し楽しかった。

脊振山 猟師岩鼻

腕時計の買い替え

Pebble Time Round から Apple Watch へ。

Apple Watch

朝駆け登山

明け方前に出発して日の出を山頂で見るというやつをやった。

天山から見る日の出

友だちとキャンプ

家族以外とのキャンプは初めてだった。複数の男手がある状態でのキャンプは楽だった(力仕事を分散)。食事は taketin さんが作ってくれて超楽だった。

ピザを焼く taketin さん

ジョギング再開

年末から走るようになった。Apple Watch でリング閉じたい病にかかった。走るときに聞くのは Podcast よりも音楽の方が良い( Podcast だと気が散る)。

一切れ 3000 円の肉を焼く

肉屋で ¥880/100g のリブロースを厚さ 3cm でカットしてもらった。良い肉を低温調理すると柔らかくなりすぎてブヨブヨになった。和牛は普通に焼いてうまくなるように育てられてるのかも知れない。

一切れ 3000 円の肉

Rebuild サポーターに登録

RAW エピソードと Extra エピソードが聞けて全文検索できるようになった。全文検索できると N さんが昔言ってたあの話をもう一回聞きたいというときに便利。サポーター向けの機能差別はもうちょいあってもよいのではと思ったけど( Rebuild を聞くに際してお金払わなくても困ることがない)、実利を得るためというより支援のためにみんな入っているのかな。

できなかったこと

こっちは箇条書きで。

  • Lokka の ActiveRecord 化を master に merge
    • あと一歩のところで燃え尽きてしまった
  • iOS / Mac のプログラミング
    • 毎年やりたいと思ってやれない
    • Xcode がでかすぎる&重すぎる
  • Rust の勉強
    • ちょびっとやってたけど難しくて途中でやめてしまった
  • 英会話の勉強
    • 会社の制度でレアジョブを始めたけどなかなか時間を捻出できない
    • 休みには休みたくなってしまう
  • ブログを 100 記事書く
    • 11 月に重めの翻訳記事を書いて燃え尽きてしまった
  • 庭の屏の塗り替え
    • ウッドデッキの流れで塗り替えたかったが梅雨入りして夏が来て冬になってしまった
  • 体重の維持
    • 前回リモートワークをしていたとき( Kaizen Platform 時代)はリモートワークしながら減量できたのに今回は太ってしまった
  • ソロでハンモック泊
    • ハンモックを買ったはいいが、ソロで山に泊まりに行けていない
  • 高い山に登る
    • 今年は標高 1500m 以上の山(ハワイーの Pu'u Kalepeamoa は除く)に登っていない
    • 火山規制が解除されたので阿蘇高岳に登りに行きたかったが、休日にソロでの外出はなかなか難しい
    • 年に一回は久住か祖母山、九州脊梁の山(標高 1700m 程度)に登りたい
  • 仕事でめざましい成果を残す
    • いつも通り

いつものようにあまりパッとしない一年だった。

| @ブログ

先日 secondlife さんのブログについて触れたが、最近は r7kamura さんも頻繁にブログを書かれるようになった。

ブログハック的な記事を良く書かれていて読むのが楽しい。特に画像にキャプションを入れる手法が参考になった。

Markdown のパース & HTML への変換を Markdown パーサー任せにするのではなく、 html-pipeline.gem を使って良い感じに挟み込みたい処理を入れているようだ。

このブログでも結構ごちゃごちゃやっている。例えばコードスニペットのシンタックスハイライトがそうだ。 Markdown から HTML にしたあと Nokogiri で HTML を読み込んで解析し、コードスニペットをハイライトされた <code> ブロックに置換している。

これに加えて OGP 読み込み君も存在しているし、 Amazon Associate のタグを読み込んでアフィリエイトリンクに置換するための処理も存在している。

いまのところそれぞれ処理を Lokka プラグインとして実装しているので一つ一つが独立して動く必要があり、 Nokogiri による HTML の解析回数が無駄になっている。 r7kamura さん方式で、ルールだけそれぞれのモジュールで定義して、 HTML の解析と置換処理は 1 回で済むようにするのが効率的だと思う。


2010 年以降も自前でブログを運用してハックの様子をシェアしてくれている hail2u.netlowreal.net はとても参考にしているし、勝手に同志のような感覚を抱いていた。自分も細々と頑張ってはいたが、ながしまきょうさん、 cho45 さんのほかには同じようにブログの運用に情熱を持っている人を見つけられなくて少しさみしい気持ちがしていたところ、 secondlife さんや r7kamura さんが加わってきて盛り上がってきた。 Blog Hacks 2020 の様相を呈してきている。