Obsidianプラグインを自作してブログの画像管理を自動化した

ObsidianからCloudflare R2に画像を直接アップロードするプラグイン「obsidian-cloudflare-r2-sync」の開発背景・設計上のこだわり・セットアップ手順をまとめました。
このブログは Obsidian でMarkdownを書いて記事を管理しています。テキストはそれで快適に書けるのですが、画像だけはずっと面倒が残っていました。記事に画像を入れるたびに別ツールでWebPに変換して、ストレージにアップロードして、URLをMarkdownに貼って——という手順を毎回繰り返すのが面倒で、記事を書くリズムが途切れる一因になっていました。
それを解消するために Obsidian プラグインを自作しました。プラグインの名前は obsidian-cloudflare-r2-sync。Obsidianのエディタに画像をドロップするだけで、WebP変換・Cloudflare R2へのアップロード・MarkdownへのURL挿入まで自動でやってくれるプラグインです。
Obsidian plugin for syncing images to Cloudflare R2
- cloudflare-r2
- obsidian
- obsidian-plugin
画像管理の何が問題だったか
ObsidianでMarkdownを書いてAstroに読み込ませるワークフロー自体はとても快適です。ファイルはリポジトリにあってGitで管理され、エディタを切り替える必要もない。ただし、画像だけはずっと問題が残っていました。
ローカルパスがAstroと合わない。 Obsidianに画像をドロップすると、Vaultのattachmentsフォルダに保存されて ![[image.png]] という形式でリンクが挿入されます。でもAstroは src/content/posts/ 以下のMarkdownを読むので、Vaultのどこに画像があっても相対パスが噛み合いません。
Gitにバイナリを置きたくない。 PNGやJPEGをリポジトリに含めると、クローンが重くなりGitの履歴も汚れます。記事に画像が増えるたびにリポジトリが肥大していくのは避けたかったです。
WebP変換が面倒。 Webに載せる画像はWebPに変換したほうがサイズを抑えられます。画像を用意するたびに別ツールで変換してリネームして、という手順を繰り返すのが地味に億劫でした。
カバー画像のfrontmatter管理が煩雑。 AstroはOGP画像をfrontmatterの cover フィールドで管理しているんですが、画像をR2にアップロードしてURLをコピーして、フロントマターに貼るという操作が毎回必要でした。
R2へアップロードしてURLに差し替えるプラグイン自体は、Image Upload Toolkit や Custom Image Auto Uploader など既にいくつか存在していました。ただ、ドロップ即アップロード・WebP変換・カバー画像のfrontmatter書き込み・認証情報のキーチェーン保存など、自分のAstroブログ向けワークフローに必要な機能をまとめて満たすものは見つからなかったので、自作することにしました。
なぜCloudflare R2なのか
このブログはすでにCloudflare Workersでホストしていたので、Cloudflareのエコシステムに乗っていくのが自然でした。R2を選んだ理由は実用的なものです。
無料枠が大きい。 R2は10GB/月のストレージで、EgressもS3と違って無料です。個人ブログの画像配信程度なら、無料枠から出ることはあまりないと思います。
S3互換API。 R2はAWS S3と互換性があるので、JavaScriptの @aws-sdk/client-s3 がそのまま使えます。
Cloudflare CDN経由で配信できる。 R2のバケットにカスタムドメインを割り当てれば、Cloudflareのグローバルネットワーク経由で画像を配信できます。このブログでは media.imaikosuke.com というサブドメインをR2バケットに向けています。
プラグインの機能
4つのコマンドで構成されています。
| コマンド | 役割 |
|---|---|
Sync images to R2 | アクティブノートのローカル画像を一括アップロード |
| Auto-upload on drop | ドロップと同時にR2へアップロード(常時ON/OFF可) |
Upload cover image | カバー画像をR2にアップしてfrontmatterに書き込む |
Delete R2 images | ノート内のR2画像を削除してリンクも消去する |
ドラッグでそのままアップロード
個人的に一番使っている機能です。ローカルの画像をObsidianのエディタにドロップすると、バックグラウンドでR2にアップロードされ、カーソル位置に  が挿入されます。
[ローカルの画像.png] ↓ エディタにドラッグ&ドロップ ↓ WebPに変換(quality 0.8) ↓ R2にアップロード(IfNoneMatch: "*") ↓ を挿入アップロードに失敗した場合はローカルの添付ファイルとして保存されてローカルリンクが挿入されます。失敗してもノートが空になることはないので安心して使えます。
Sync images to R2
すでにローカル画像を参照しているノートをまとめてR2に移行したいときに使うコマンドです。アクティブノートのMarkdownを走査してローカル画像を検出し、まとめてR2にアップロードしてリンクを差し替え、元のローカルファイルをObsidianのゴミ箱に移動してくれます。
対応している記法はMarkdown標準の  とObsidian独自の ![[image.png]] 記法の両方です。WikiスタイルのリンクはMarkdown形式に変換されます。すでに https:// から始まるリモートURLはスキップされます。
処理が終わると Image sync: 3 uploaded, 1 skipped, 0 failed のようなNoticeが表示されるので、結果がひと目でわかります。
Upload cover image
カバー画像のアップロードに特化したコマンドです。コマンドパレットから実行するとファイルピッカーが開き、画像を選択するとR2にアップロードしてfrontmatterに書き込んでくれます。
コマンドパレット → "Upload cover image" ↓ ファイルピッカーでhero.pngを選択 ↓ R2にアップロード(coverプレフィックスで別管理) ↓ frontmatterに自動書き込み---cover: https://media.imaikosuke.com/cover/2026/06/20260602-hero.webp---本文の画像とカバー画像で、アップロード先のパス(Object key template)を独立して設定できます。私はカバー画像を cover/ プレフィックスで管理して本文画像とは別のパスに置いています。
Delete R2 images
アップロードした画像をCloudflare R2から削除するためのコマンドです。アクティブノート内のR2のパブリックURLを検出して、確認モーダルを出してからR2のオブジェクトを削除し、ノートのリンクも消去します。frontmatterの cover: 行ごと削除にも対応しています。

設計上のこだわり
WebP自動変換はブラウザAPIだけで完結させる
PNG / JPEG / JPG / BMP をアップロード前に自動でWebPに変換しています。外部ライブラリは使わず、ブラウザネイティブの OffscreenCanvas APIだけで実装しました。
const bitmap = await createImageBitmap(file)const canvas = new OffscreenCanvas(bitmap.width, bitmap.height)const ctx = canvas.getContext('2d')!ctx.drawImage(bitmap, 0, 0)const blob = await canvas.convertToBlob({ type: 'image/webp', quality })ObsidianはElectronベースのデスクトップアプリなので OffscreenCanvas が使えます。sharp などのNode.jsライブラリを入れてバンドルサイズを増やさなくていいのがいいところです。
変換クオリティはスライダーで設定できます(0.5〜1.0、デフォルト0.8)。本文画像とカバー画像で変換のON/OFFとクオリティを独立して設定できるようにしました。デフォルトは本文画像が変換ON、カバー画像は変換OFFにしています。カバー画像は後から手動でリサイズしたいこともあるので、そこは手元に残せるようにしておきたかったです。
Object key templateでパスを自由に組み立てる
R2上のオブジェクトキー(保存パス)をテンプレートで設定できます。デフォルトは {year}/{month}/{timestamp}-{filename} で、たとえば 2026/06/20260602230000-image.webp のようなパスになります。
| プレースホルダー | 内容 |
|---|---|
{year}, {month}, {day} | 日付(ローカル時刻) |
{hour}, {minute}, {second} | 時刻 |
{timestamp} | YYYYMMDDHHmmss 形式 |
{filename} | 正規化されたファイル名(小文字・安全な文字のみ) |
{slug} | ノートのファイル名(拡張子なし、正規化済み) |
{notepath} | Vault内の相対パス(正規化済み) |
{hash} | アップロードバイト列のSHA-256先頭12文字 |
{uuid} | アップロードごとのUUID v4(ハイフンなし) |
{hash} は Web Crypto APIの crypto.subtle.digest('SHA-256', ...) で計算しています。これも外部ライブラリなしで実装できました。
クレデンシャルをObsidianのキーチェーンに保存する
R2へのアクセスに必要なAPIキーをどこに保存するかは慎重になる必要があります。設定画面のテキストフィールドに直接貼り付けると、data.json(Obsidianの設定ファイル)に平文で保存されてしまいます。そうすると、GitHubにVaultを同期している場合に漏洩するリスクがある。
Obsidianは secret storage(OSのキーチェーン)にアクセスするAPIを2026年1月に提供しています。このプラグインではアクセスキーIDとシークレットアクセスキーをキーチェーンに保存し、設定画面にはキー名のみを表示するようにしました。
設定ファイルには名前だけが残り、実際の値はOSのセキュアなストレージに入ります。VaultをGitHubにpushしても認証情報が漏れない設計です。
重複アップロードをIfNoneMatchで防ぐ
同じ画像を繰り返しアップロードしてしまわないよう、PutObjectリクエストに IfNoneMatch: "*" ヘッダーを付けています。これはR2のS3互換APIの機能で、同じオブジェクトキーにすでにオブジェクトが存在する場合はHTTP 412を返します。プラグイン側ではこれを ObjectAlreadyExistsError として捕捉して、スキップ扱いにしています。
await client.send( new PutObjectCommand({ Bucket: bucketName, Key: objectKey, Body: buffer, ContentType: 'image/webp', CacheControl: cacheControlValue, IfNoneMatch: '*' }))CacheControl も一緒に設定できます。デフォルトは public, max-age=31536000, immutable(1年キャッシュ)で、画像のURLがオブジェクトキーに基づいて固定されているので、キャッシュを積極的に効かせられます。
アップロード前にプレビューできるSyncモーダル
設定でONにすると、Sync images to R2 を実行したときにアップロード前のプレビューモーダルが開きます。対象の画像一覧・アップロード後のオブジェクトキー・パブリックURLを確認してから、チェックボックスで選択してアップロードを実行できます。
「バッチ処理で一気にやってほしいけど、たまに確認したい」というときに便利で、設定でデフォルトをどちらにするか選べます。

セットアップ手順
1. Cloudflare R2のバケットを作成する
Cloudflareのダッシュボードから R2 → バケットを作成します。バケット名は何でもよいです(例:blog-media)。
作成したら、Settings → Public access でバケットをパブリックに設定するか、カスタムドメインを割り当てます。カスタムドメインを使う場合は、使いたいサブドメイン(例:media.example.com)をCloudflareで管理しているドメインに向けて設定します。
2. APIトークンを発行する
Cloudflareダッシュボードの R2 → Manage R2 API Tokens からトークンを作成します。
必要な権限は以下の通りです。
| 権限 | 用途 |
|---|---|
| Object Read & Write | 画像のアップロード・削除 |
トークン作成後に Access Key ID と Secret Access Key が表示されるので、安全な場所に控えておいてください(この画面を閉じると再表示できません)。
3. プラグインをインストールする
Obsidianのコミュニティプラグインとして公開されているので、設定画面から直接インストールできます。
- Obsidianの 設定 → コミュニティプラグイン → コミュニティプラグインを閲覧 を開く
- 「Cloudflare R2 Sync」を検索してインストールする
- インストール後にプラグインを有効化する
4. プラグインの設定をする

プラグインを有効化したら設定画面を開きます。設定項目は大きく3グループあります。
R2の接続設定(必須)
| 設定項目 | 説明 |
|---|---|
| Access Key ID | ステップ2で取得したAccess Key ID |
| Secret Access Key | ステップ2で取得したSecret Access Key |
| Bucket name | 作成したバケット名 |
| Account ID | CloudflareのAccount ID(ダッシュボードのURLに含まれています) |
| Public URL | https://media.example.com のようなR2のパブリックURL |
本文画像の設定
| 設定項目 | 説明 |
|---|---|
| Object key template | アップロードパスのテンプレート(デフォルト: {year}/{month}/{timestamp}-{filename}) |
| Convert to WebP | WebP変換のON/OFF(デフォルト: ON) |
| WebP quality | 変換クオリティ(0.5〜1.0、デフォルト: 0.8) |
| Auto-upload on drop | ドロップ時の自動アップロードのON/OFF(デフォルト: ON) |
| Show sync preview | Syncコマンド実行時にプレビューモーダルを出すかどうか |
| Cache-Control | アップロード時に付与するCacheControlヘッダー |
カバー画像の設定
本文画像と同じ項目が独立して設定できます。カバー画像用のObject key templateを cover/{year}/{month}/{timestamp}-{filename} のように分けておくと、R2上でカバー画像を一箇所にまとめられます。
設定が完了したら、Obsidianで適当なノートを開いて画像をドロップしてみてください。R2のパブリックURLに差し替わったリンクが挿入されれば動作確認OKです。
実際のワークフローがどう変わったか

作る前は、記事に画像を入れるたびに次のような操作をしていました。
- 画像を用意する
- WebPに変換する
- R2のダッシュボードにブラウザからアップロードする
- パブリックURLをコピーする
- ObsidianのMarkdownにURLを貼り付ける
カバー画像のときはさらにfrontmatterを手動で書き換える手順が加わります。
プラグインを作ってからは、ドロップするだけで完結するようになりました。Obsidianのエディタを離れることもなく、WebPへの変換もURLのコピーも自動でやってくれます。記事を書くリズムが途切れなくなったのが一番大きいです。
ブログを書き続けることを考えると、書くこと自体への摩擦をできる限り減らしたいと思っています。画像まわりの手間はその摩擦のひとつだったので、自動化できてよかったです。
まとめ
Obsidianからブログ記事を書くワークフローにおいて、画像管理の手間を一掃するために obsidian-cloudflare-r2-sync を自作しました。
- ドラッグ&ドロップ一発でR2へ: ドロップした瞬間にWebP変換→R2アップロード→URL挿入まで完結
- 既存ノートの一括移行:
Sync images to R2コマンドでローカル画像をまとめてR2に移せる - カバー画像も自動化: frontmatterへの書き込みまで含めてコマンド一発
- セキュアな設計: APIキーはOSのキーチェーンに保存し、Vaultをpushしても漏れない
- 外部ライブラリ不要: WebP変換も重複チェックも、
OffscreenCanvasと Web Crypto API だけで実装
Obsidianのコミュニティプラグインとして公開しているので、同じような構成でブログを運用している方はぜひ試してみてください。フィードバックや不具合報告はGitHubのIssueへどうぞ。
Obsidian plugin for syncing images to Cloudflare R2
- cloudflare-r2
- obsidian
- obsidian-plugin