0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像変換を使った「お知らせCMS」をつくってみた:管理画面から画像を上げるだけで、一覧も記事も軽くなる

0
Last updated at Posted at 2026-10-26

対象: 前の記事で画像変換のURLを試して、「で、更新する人にとって、どう便利になるの?」が気になった方
所要時間: 40〜60分 / 使うもの: ブラウザ、ターミナル、Supabaseのプロジェクト
確認日: 2026年10月(画面や料金は変わることがあります)
前の記事(手順編): https://qiita.com/tomozilla/items/4521efc558885d091743
ビジネス編(やさしい解説・note): https://note.com/tomozilla/n/na726fb1ebab5
サンプルコード一式(GitHub): https://github.com/tomozilla/storage-image-transform-demo
記事の中のコードは日本語コメントです。リポジトリの本体は英語コメントで、日本語コメント版(この記事と同じもの)は ja/ フォルダーにあります。

前の記事では、画像変換のURLを手で組み立てて、データ量がどれくらい減るかを測りました。でも、実際にお知らせを更新する人は、URLのパラメータなんて組み立てたくありません。

更新する人がやりたいのは、画像を1枚アップロードして、「公開」を押すだけ。そして、見る人には、一覧は軽く、記事は読みやすい大きさで出てほしい。そんな「お知らせCMS」を、管理画面と公開ページの2枚でつくってみました。

0. 先に伝えておきたいこと(正直ベース)

  • 画像変換は、ドキュメントでは有料プラン(Pro以上)の機能です。 料金表(2026年10月時点)では、Freeプランは「利用不可」、Pro/Teamは変換元の画像100枚までが枠内で、超えると1000枚あたり5ドル、と書かれています。ところが、私のFreeプランのデモでは、変換が動きました(前の記事のとおり、理由はわかりません)。今後は変わるかもしれません。この動きを当てにせず、最新の情報を確認してください。 私は課金やアップグレードをしていません。
  • そのため、公開ページには、変換が使えなかったとき、元の画像にもどす仕組みを入れました(画面にも、「元の画像を表示しています」と出します)。
  • 費用の見積もり(計算した目安):Proの料金表を、そのまま読むと、変換する元の画像が、1か月に1,000枚あれば、枠の100枚を引いた900枚が超過です。超過分の請求の丸め方(1000枚単位の切り上げか)は、私は確認していません。ストレージ容量(Freeは1GB)や、通信量(Freeは5GB)も、別に数えられます。
  • サンプル画像は、生成した絵で、色が平らな部分が多いです。実際の写真だと、ここまで小さくなりません。
  • 未検証:JavaScriptライブラリの transform オプション、非公開バケットの署名付きURL、Proプランでの動き、実際の写真、スマホでの表示。

1. つくったもの

管理画面(編集者向け)

メールアドレスとパスワードで、編集者がログインします。

編集者ログイン

ログインしたら、タイトル・本文・カバー画像を入れて、保存します。画像を選んで「保存する」を押すと、こう出ます。

画像をアップロードして保存した直後の管理画面

アップロードした画像:406 KB → 一覧では 5.6 KB の画像として配信されます(元の1%)

保存の直後に、一覧で実際に配信される画像の大きさを、画面が取得して見せてくれます。「画像を軽くしておいてください」と、編集者にお願いする必要がありません。

画像以外のファイルを選ぶと、こうなります。

画像ではないファイルは、保存前に断る

公開ページ(見る人向け)

お知らせの一覧。サムネイルは、元の約1%

3つのお知らせが、サムネイルつきで並びます。それぞれの下に、「この画像:4.1 KB(元は414 KB・1%)」のように、実際に届いた大きさを出しています(デモ用の表示です)。

記事を開くと、カバーが大きめで出ます。

記事ページ。カバーは幅1200・画質75で、17KB

同じ406KBの元画像から、**一覧には5.6KB(幅400)、記事には17KB(幅1200・画質75)**が配られました。サイズ違いの画像を、編集者が何枚も作る必要はありません。

全体像

2. つくり方

app/index.html      公開ページ(一覧・記事)
app/admin.html      管理画面
sql/app/01_cms.sql  編集者のためのポリシーと、元画像のサイズを覚える列

データベース:編集者のポリシー

「ログインした人は、編集者」とみなして、記事とカバー画像を操作できるようにします。

alter table public.posts add column if not exists cover_bytes int;   -- 元画像のサイズ

create policy "editors can read all posts" on public.posts for select to authenticated using (true);
create policy "editors can insert posts"   on public.posts for insert to authenticated with check (true);
create policy "editors can update posts"   on public.posts for update to authenticated using (true) with check (true);
create policy "editors can delete posts"   on public.posts for delete to authenticated using (true);

create policy "editors can upload covers" on storage.objects for insert to authenticated with check (bucket_id = 'cms-images');
create policy "editors can read covers"   on storage.objects for select to authenticated using (bucket_id = 'cms-images');
create policy "editors can delete covers" on storage.objects for delete to authenticated using (bucket_id = 'cms-images');

公開済みの記事を誰でも読める、前の記事のポリシー(anyone can read published posts)は、そのままです。

これはデモ用です。 「ログインした人は全員が編集者」になっています。本物のアプリでは、編集者だけを入れるロールや表で絞ってください。

管理画面:アップロードして、記事を保存する

cover_path = `covers/${crypto.randomUUID()}.${ext}`                       // 名前は、ランダムにする
await supabase.storage.from('cms-images').upload(cover_path, file, { contentType: file.type, upsert: false })
await supabase.from('posts').insert({ title, body, cover_path, cover_bytes: file.size, published })

画像の種類(PNG / JPEG / WebP)と大きさ(10MB)は、保存の前に画面で確認しています。ただし、これは画面での確認だけです(ポリシーでは制限していません)。

公開ページ:用途ごとにURLを変える

// 一覧のサムネイル: 幅400、全体が収まるように縮小
`${base}/render/image/public/cms-images/${path}?width=400&resize=contain`
// 記事のカバー: 幅1200、画質75
`${base}/render/image/public/cms-images/${path}?width=1200&resize=contain&quality=75`

前の記事の「width だけだと切り取られる」を踏まえて、resize=contain を付けています。変換に失敗したら、元の画像(/object/public/...)にもどします。

動かすには、前の記事と同じ要領です。

cd app
cp config.example.js config.js     # URL・publishableキー・バケット名を入れる
python3 -m http.server 8765 --bind 127.0.0.1
# 公開ページ: http://127.0.0.1:8765/index.html  /  管理画面: .../admin.html

3. つまずいたこと

① 「削除」しても、画像ファイルが消えていなかった(一番大きなつまずき)

管理画面に「削除」ボタンを付けて、お知らせを削除してみました。記事の行は消えて、一覧からもなくなります。ところが、ストレージを見ると、画像ファイルが残っていました。

select name from storage.objects where bucket_id = 'cms-images' order by name;
-- cover-sample.png
-- covers/4533e0fc-….png    ← 消したはずのお知らせの画像
-- covers/ab323272-….png    ← 同じく

画面はエラーを出していませんでした。supabase.storage.remove() が、**エラーなしで「0件削除」**を返していたのです。

原因は、権限でした。削除するには、削除(DELETE)の権限だけでなく、読む(SELECT)の権限も必要でした。最初は、アップロードと削除のポリシーしか作っていませんでした。なので、対象のファイルが「見えない」ことになり、何も消えなかったようです。

直し方は2つです。

  1. 読む(SELECT)のポリシーを足す(上の editors can read covers)
  2. 画面の側で、「ほんとうに消えたか」を確かめる(remove() の結果の件数を見る)
async function removeCover(path) {
  const { data, error } = await supabase.storage.from(BUCKET).remove([path])
  return !error && Array.isArray(data) && data.length === 1   // 1件消えたことを確かめる
}

直したあと、アップロード → 削除をもう一度試したら、画像ファイルも消えて、警告も出ませんでした。前に残っていた2つは、ログインしたクライアントから削除しました。

学び:「エラーが出ない」は「成功した」ではない。 特に、権限が絡む削除は、結果を確かめる。残ったファイルは、容量の費用にもなり、見えないところに増えていきます。

② 記事の保存に失敗したら、アップロードした画像が残る

画像を先にアップロードして、そのあと記事を保存しています。記事の保存が失敗すると、画像だけが残ってしまいます。なので、保存に失敗したら、画像を消すようにしました(上の removeCover)。ただ、この失敗の経路を、わざと起こして確かめることは、私はしていません。

③ 公開バケットの画像は、URLを知っていれば誰でも見られる

記事が「下書き」でも、画像のURLを知っている人は、画像を見られます(公開バケットだからです)。下書きの段階の画像を、見せたくない場合は、公開バケットは向きません。署名付きURLと変換の組み合わせが、ドキュメントにありますが、私は試していません。

④ 大きさの表示は、ダウンロードして測っている

一覧の「この画像:4.1KB」は、画像を取得して、その大きさを測っています。デモとしては見やすいのですが、本番の画面には要らない表示です。

4. 実際のCMSにするなら

どれも、私は試していません。

  • 編集者のロール:ログインした人全員ではなく、編集者だけ
  • 画像のチェック:種類は、ファイルの中身で確かめる(画面での確認だけでは、すり抜けられます)。バケット側にも、大きさと種類の制限を設定できるようです(私は確認していません)
  • 残ったファイルの掃除:記事に使われていない画像を、定期的に見つけて消す
  • 画面サイズに合わせた配信:スマホには小さく、大きな画面には大きく(複数サイズを用意する)
  • 費用の見張り:変換する元の画像の枚数、ストレージ容量、通信量
  • 非公開の画像:署名付きURLで、期限つきで見せる

まとめ

  • 管理画面から画像を1枚アップロードするだけで、一覧は約5KB、記事は約17KBで配れた(サンプル画像での、私の測定。実際の写真では、ここまで小さくなりません)
  • 編集者に「画像を軽くしてね」とお願いする必要がなくなる
  • 変換は有料プランの機能とされている。私のFreeでは動いたが、当てにしない。変換が使えないときの元の画像へのもどし先を用意した
  • 削除は、権限が足りないと、エラーなしで0件になる。 結果を確かめる
  • 公開バケットの画像は、URLを知っていれば誰でも見られる

コードは、上のGitHubにあります。これで、6つのテーマの「手順編」と「アプリ編」が、そろいました。


スクリーンショットは、記事用に作った無料プランのデモ環境で撮影したものです。サンプルの画像は、生成した絵です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?