AetherEchoesEngineering
Vol.042026年5月9日
Engineering#00581 min4775 view

CarrierWave + MiniMagick で OGP / サムネを自動派生する

アップロード 1 枚から OGP / カード / サムネを自動生成する Uploader 設計。WebP 出力、CLS を抑える dimensions、CDN キャッシュ。

SoSoraEndo2026年5月9日1 min477

なぜ自動派生を回すのか

記事に画像を入れるとき、必要なバリアントは概ね決まっている:

  • thumb — 一覧カード用、200×200 or 400×250 程度
  • eyecatch — 記事ヒーロー用、960×540 程度
  • og — SNS シェア用、1200×630 固定

毎回手動で書き出すのは面倒だし、忘れる。アップロード時に自動で派生を作る 仕組みを 1 度組めば、後は元画像を投げるだけになる。

私は CarrierWave + MiniMagick で組んでいる。シンプルに動く。

基底アップローダ

# app/uploaders/base_uploader.rb
class BaseUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  storage :file  # 開発: ローカル / 本番: :fog で S3 に切替

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def filename
    @name ||= "#{secure_token}.#{file.extension}" if original_filename.present?
  end

  protected

  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) ||
      model.instance_variable_set(var, SecureRandom.uuid)
  end
end

secure_token で UUID ベースのファイル名にする。元のファイル名から推測されない。

variant 定義

class MediaAssetFileUploader < BaseUploader
  process :auto_orient, :strip

  version :thumb do
    process resize_to_fill: [400, 250, gravity: 'Center']
    process quality: 80
    process convert: 'webp'
  end

  version :eyecatch do
    process resize_to_fit: [960, 540]
    process quality: 85
    process convert: 'webp'
  end

  version :og do
    process resize_to_fill: [1200, 630, gravity: 'Center']
    process quality: 85
    process convert: 'jpg'  # OGP は webp 非対応 SNS が多い
  end

  def extension_allowlist
    %w[jpg jpeg png webp gif]
  end
end

ポイント 3 つ:

  1. auto_orient で iPhone 撮影画像の回転メタデータを反映する
  2. strip で EXIF メタデータを削除(プライバシー + サイズ削減)
  3. og だけは jpg で出す。Facebook / X の OGP サムネイル生成が webp 非対応のことがある

OGP は jpg、それ以外は webp

バリアントフォーマット理由
original元のままアーカイブとして残す
thumbwebpサイト内のみ。サイズ最小化
eyecatchwebpサイト内のみ。Next/image が AVIF 変換
ogjpg外部 SNS スクレイパに優しく

モデル側

class MediaAsset < ApplicationRecord
  mount_uploader :file, MediaAssetFileUploader
end

これだけ。あとは media_asset.file.thumb.url media_asset.file.eyecatch.url media_asset.file.og.url でアクセスできる。

アップロード API

# Bot::MediaController#create
def create
  asset = MediaAsset.new(file: params[:file], alt: params[:alt])
  asset.save!
  render json: {
    id: asset.id,
    urls: {
      original: asset.file.url,
      thumb: asset.file.thumb.url,
      eyecatch: asset.file.eyecatch.url,
      og: asset.file.og.url,
    }
  }
end

POST 一発で 4 種類の URL が返る。フロント側はこの中から場面に応じて使い分ける。

まとめ

CarrierWave + MiniMagick の自動派生は、コード量にして 30 行で「画像配信の悩みごと」を 9 割消せる。残り 1 割は responsive srcset と blur placeholder(BlurHash / ThumbHash)だが、それは別記事で。

画像処理は最初の 1 日で組むと、後の数十時間が浮く。

Tags

Reaction

Share

X (Twitter)