LumeCMSでWebP変換を行う

5 min read

こんにちは、無能です。
今までWordPressを使っていたこともあり、WebP変換はプラグインを導入して内部的にはImageMagickを実行してかんたんに行っていましたがそんな機能はLumeCMSでは存在しません。

もしかすると、Lumeのプラグインで可能かもしれませんが結果的には画像処理として行う効率性からCで書かれたものから実行した方がより高速で効率的です。
Nginx側のスクリプトで不可能ではありませんが、リバースプロキシを行っているということもあり他にも導入する必要があったことと、リバースプロキシを行っているサーバーのリソースは1vCPU Mem 1GB Disk 25GBとかなり限られたリソース内で無理して行う必要性を感じませんでした。
また、内部的にはLuaスクリプトを実行することで可能にしている方もいるようです。 Nginx+Lua+libwebpによるサーバー画像の自動WebP変換の実現
欠点としてはリクエスト時にこのスクリプトが実行されるのでリソースが集中する可能性を秘めているように感じてしまいます。

npmとしてのラッパーはcwebp-bin というものがあるようですが、実際必要なのは画像のコンバートとビルドされた静的サイトのhtmlファイルの画像パスのrewriteです。

そこで、バックエンドの処理としてLumeの変更を監視し自動でビルドしているinitデーモンのスクリプトを自分で作ったものの中でwebpに変換するシェルスクリプトを実行することにします。
また、libvipsは使っていません。あくまでlibvipsのWebP変換には内部的にlibwebpを使っているだけでapt等から楽にインストール出来るcwebpと同じものであるので、cwebpで変換を行います。

変換するシェルスクリプト

以下の様に重複パターンを除き画像ファイル群をWebPに変換します。

#!/bin/bash

# 監視するディレクトリ
SOURCE_DIR="/var/www/html/soulmining/src/uploads"

# WebP出力先ディレクトリ
DEST_DIR="/var/www/html/soulmining/src/uploads"

# 変換対象の拡張子
EXTENSIONS=("png" "jpg" "jpeg")

# WebPの品質設定 (0-100)
QUALITY=80

# 必要なコマンドの確認
command -v cwebp >/dev/null 2>&1 || { echo >&2 "cwebpコマンドが見つかりません。インストールしてください。"; exit 1; }

# 出力先ディレクトリが存在しない場合は作成
mkdir -p "$DEST_DIR"

# ファイルを処理する関数
process_file() {
    local file="$1"
    local filename=$(basename "$file")
    local name="${filename%.*}"
    local dest_file="$DEST_DIR/${name}.webp"

    # 既に変換済みのファイルはスキップ
    if [ -f "$dest_file" ]; then
        echo "スキップ: $filename (既に変換済み)"
        return
    fi

    # WebPに変換
    cwebp -q $QUALITY "$file" -o "$dest_file"
    
    if [ $? -eq 0 ]; then
        echo "変換成功: $filename -> ${name}.webp"
    else
        echo "変換失敗: $filename"
    fi
}

# メイン処理
for ext in "${EXTENSIONS[@]}"; do
    find "$SOURCE_DIR" -type f -name "*.$ext" | while read file; do
        process_file "$file"
    done
done

echo "処理完了"

これをinitデーモンの/etc/init.d/lume-watcherに追記します。

#!/bin/bash
### BEGIN INIT INFO
# Provides:          lume-watcher
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Watches directory and triggers Lume task
# Description:       Watches the specified directory and triggers the Deno Lume task when changes are detected.
### END INIT INFO

WATCHED_DIR="/var/www/html/soulmining/src/"
COMMAND="/home/haturatu/.deno/bin/deno task lume --dest=site"
CONV_WEBP="/opt/sh/webp.sh"
~~~
省略
~~~
# 監視開始を行う
monitor_directory() {
    inotifywait -m -r -e modify,create,delete "$WATCHED_DIR" | while read -r directory events filename; do

        echo "$(date): Change detected" >> "$LOG_FILE"
        last_run_time=$(get_last_run_time)
        now=$(current_time)

        if [ $((now - last_run_time)) -ge $COOLDOWN_TIME ]; then
            check_and_rotate_log

            echo "$(date): Executing command" >> "$LOG_FILE"
            
            sleep 0
            cd $OUTPUT_ROOT_DIR || exit
            $CONV_WEBP >> "$LOG_FILE" 2>&1
            $COMMAND >> "$LOG_FILE" 2>&1
            cd ~ || exit

            set_last_run_time
        else
            echo "$(date): Command not executed due to cooldown" >> "$LOG_FILE"
        fi
    done
}
~~~
以下略

厳密には、このinitデーモンの場合は対象ディレクトリ内の変更を全て検知するので画像アップロードしただけで再ビルドが走りますが、まあしょうがないでしょう。
やるならば、inotifywaitの実行時のオプションを --exclude "dir/path"として指定あげましょう。

_config.tsに追記

さて、まだ仕事は残っています。
このままだとLumeでビルドしたときのファイルのHTMLファイルのパスはオリジナル画像のパスを指定しています。
ということでビルド時にrewriteしてくれるようにしましょう。

import { walk } from "https://deno.land/std/fs/mod.ts";
~~~
略
~~~
site.addEventListener("afterBuild", async () => {
  const buildDir = site.dest(); // ビルド出力ディレクトリ

  for await (const entry of walk(buildDir, { exts: [".html"] })) {
    if (entry.isFile) {
      let content = await Deno.readTextFile(entry.path);

      // URLを.webpに変換する正規表現
      const regex = /(src=\/uploads\/[^"'\s]+\.(png|jpe?g|gif))/gi;

      content = content.replace(regex, (match) => {
        return match.replace(/\.(png|jpe?g|gif)$/i, '.webp');
      });

      await Deno.writeTextFile(entry.path, content);
      console.log(`Processed: ${entry.path}`);
    }
  }
});

ぶっちゃけこれもlume-watcherの中でsedで書き換えたほうが短いし早そうだし楽そうだな・・・と思ったのは内緒です。

ちなみにlumeの-wオプションは安定しない

私の実行しているデーモンがありますがLume公式でも一応watchするということで-wオプションをつけてdeno task lume -wとして起動すると簡易サーバーを使わず、ファイル変更の度にHTMLを生成することもできるのですが安定しません。
私の環境下だとよくクラッシュしました。

LumeCMS自体ではcmsのレポジトリに存在する./adapters/lume.ts内の

  // Start the watcher
  const watcher = site.getWatcher();

  // deno-lint-ignore no-explicit-any
  watcher.addEventListener("change", async (event: any) => {
    const files = event.files!;
    await site.update(files);
    dispatch("previewUpdated");
  });

  watcher.start();

で呼び出しているみたいです。

await site.update(files);

のところみたいですね。
あくまでLumeCMSのコード内で呼び出されているものなので、Lume自体も同じものであるはずな気がするけどなんでだろう。

それでは。 またよろしくおねがいします。