プログラミング

ObsidianをRSSリーダー+AIニュース秘書にする自作プラグインを作りました(Miniflux Reader)

自宅サーバーに立てたセルフホスト型RSSリーダー「Miniflux」を、Obsidianの中で読むために作った自作プラグイン Miniflux Reader の紹介です。3つのレイアウトを持つ記事一覧、4段階のサムネイル解決、記事のMarkdown保存に加えて、サーバー側のスクリプトがAIに選ばせた「今日の必読」を読むキュレーション画面まで、実装のコードを抜粋しながら内部の動きを説明します。

目次

はじめに

毎日のニュースチェックに月額のクラウドサービスを払い続けていて、「これ、自分のサーバーでなんとかできないかな」と思ったことはありませんか。

私はずっと「Inoreader」という定番のクラウドRSSリーダー(世界中のニュースサイトの更新をまとめて読めるサービス)を有料で使っていました。
便利なのですが、使い込むほどに「毎月お金を払って、しかも自分の購読データをよその会社に預け続けるのはもったいないな」と感じるようになりました。
ちょうど自宅にサーバーを立てたところだったので、これを機にRSSリーダーも自前へ移そうと考えました。

そこで自宅サーバーに「Miniflux(ミニフラックス)」という無料のセルフホスト型RSSリーダーを立てたのですが、ひとつ問題がありました。
Miniflux単体はとてもシンプルで、Inoreaderの「読む体験」には遠く及ばなかったのです。
カード表示やサムネイル、サクサク読み進めるあの感じが恋しくなりました。

それなら自分で作るしかない、ということで、Inoreaderと同じような閲覧体験をObsidianの中で再現するプラグインを開発しました。
それが Miniflux Reader です。
さらにこのプラグインには、未読ニュースをAIに選ばせて要約させる「キュレーション」機能というもうひとつの顔があります。

この記事は、よくある「こんなプラグインを作りました、機能はこれです」で終わらせずに、実装のコードを抜粋しながら内部でどう動いているかまで書いてみます。
土台はTypeScriptとObsidianのプラグインAPIだけの構成です。
コードの部分は雰囲気だけ眺めて読み飛ばしても話が繋がるように、コードブロックの直後には必ず日本語で種明かしを置きます。
「使えればいい」という方も安心して読み進めてください。
逆に技術に興味がある方は、読み終わる頃には自分で同じ構成を組める程度の解像度になるかと思います。

コード一式を使ってみたい・中身を見てみたいという方は、コメントでお知らせいただければお渡しします。

このプラグインで何ができるか

ざっくり言うと、Obsidianが2種類のニュース画面を持つようになります。

このプラグインで何ができるか(2種類のニュース画面)

どちらもObsidianのタブとして開くので、メモを書きながら片方のペインでニュースを読めます。
テーマ(ライト/ダーク)にも自動で追従し、スマホでも崩れません。
記事は既読・スター・Markdown保存ができ、気になった記事はそのままノート化してメモを足せます。

フィードの購読管理(どのサイトを追加するか)はMiniflux本体に任せ、このプラグインは「読む・保存する」体験に特化する、という割り切りです。
一言でいえば、正しく集めて・気持ちよく読んで・AIに選ばせて・ノートに残すの4点セットを、Obsidianの中だけで回せるようにした道具です。

既存の手段との違い

なぜ既存のものを使わず自作したのか、比較で整理します。

既存の手段との違い

Inoreaderの「読み心地」と、Minifluxの「自前・無料・データが手元にある」という長所を両取りしつつ、読んだ記事をその場でObsidianノートに変えられること。
ここが既製品との一番の違いです。
ブラウザでニュースを読むと読んで終わりですが、Obsidianの中なら [[リンク]] で自分の他のノートとつなげられます。

解決したい課題

具体的に困っていたのは次の3つです。

  • 読む場所が分断していた:メモ・資料・タスクはすべてObsidianにあるのに、ニュースのインプットだけ外(ブラウザのMiniflux)にあって、行ったり来たりが面倒でした。
  • Minifluxの素のUIだと読み進める気が起きない:カードもサムネイルも無いリスト表示で、消化が進みませんでした。
  • 集めた未読が多すぎて結局読まない:フィードを増やすほど1日数百件になり、全部を自分でさばくのは無理がありました。3つ目をAIに肩代わりさせよう、というのがキュレーション機能の動機です。

まず完成形から

細かい話の前に、何が手に入るのかを先に見てもらいます。

「すべてのニュース」画面は、Minifluxに集めた記事をカードで並べた、Inoreaderに近い見た目です。
j/kキーで記事を上下に移動し、sでスター、mで既読にして、片手でどんどん消化できます。

リーダー画面(Discover)のカードグリッド

リーダー画面(Discover)。カードグリッドで記事が並びます。

Magazineレイアウト

Listレイアウト

マガジンとリストにも切り替えられます。

「キュレーション」画面は、AIがその日選んだ記事が「必読」と「ヘッドライン」の2部構成で並びます。
必読には1件ごとに「なぜ読むべきか」と「3行要約」が付いていて、世間一般の人気順ではなく自分にとって意味のある順で上に来ます。

キュレーション画面の時間帯タブ

キュレーション画面の必読カード

キュレーション画面。時間帯タブと必読カードが並びます。

スクロールした先のヘッドライン一覧

スクロールするとその他のヘッドラインも出てきます。

できること(全機能ツアー)

プラグインを開く入り口は2つです。
リボン(左端のアイコン列)の「すべてのニュース」アイコンと「キュレーション」アイコン、またはコマンドパレットからです。
実装上は main.ts で2つのビュー(ReaderViewCurationView)を登録し、それぞれにリボンアイコンとコマンドを割り当てています。

機能を一段ずつ並べると、こうなります。

  • 3つのレイアウト:Discover(カードグリッド)/Magazine(サムネイル+本文の横並び)/List(未読ドット付きのコンパクト一覧)
  • 記事操作:既読・未読の切り替え(一括既読も対応)、スター、Markdown保存、元記事を開く
  • 賢いサムネイル:画像情報の乏しいRSSでも、4段階のフォールバックでなるべく絵を出す
  • AIキュレーション:時間帯タブで「今日の必読」を読み、良かった記事に「いいね」を付ける
  • キーボード操作:PCでは一覧をキーボードだけで消化できる

ここから先は、肝になる部分を実装のコードと一緒に掘り下げます。

詳細①:Miniflux APIとの通信(記事取得・既読同期)

Minifluxとのやり取りはすべて src/api/miniflux-client.tsMinifluxClient クラスに集約しています。
MinifluxはREST API(URLにアクセスするだけで操作できる仕組み)を持っているので、必要なエンドポイントを叩くだけです。
認証はAPIキー(Minifluxの「設定 → APIキー」で発行する、パスワード代わりの長い文字列)を X-Auth-Token ヘッダーに載せる方式で、起動時に GET /v1/me を叩いて疎通を確認します。

主に使っているエンドポイントは次の通りです。

使用しているMiniflux APIエンドポイントの一覧

通信にはObsidianの requestUrl を使っています。
これは、Obsidianアプリから外部サーバーへHTTP通信するときにCORS(ブラウザが別ドメインへの通信を制限する仕組み)の制約を回避するための、Obsidian標準の関数です。
普通の fetch だと弾かれるケースでも requestUrl なら通せます。

APIが返してくる生のJSONは、そのままだと画面で扱いにくいので、parseEntry() で自分の Article 型に変換します。
ここがデータの入り口です。

private parseEntry(e: MinifluxAPI.Entry): Article {
    const content = e.content || '';
    const imageUrl = this.extractFirstImage(content) || this.extractEnclosureImage(e.enclosures);
    const plainText = this.stripHtml(content);

    return {
        id: String(e.id),
        feedId: `feed:${e.feed_id}`,
        feedTitle: e.feed?.title || '',
        title: e.title,
        url: e.url,
        content,
        summary: plainText.substring(0, 300),
        imageUrl: imageUrl || undefined,
        published: new Date(e.published_at).getTime(),
        read: e.status === 'read',
        starred: e.starred,
        categories: e.feed?.category ? [e.feed.category.title] : [],
        tags: [],
    };
}

日本語にすると、やっていることはこうです。

記事のHTML本文から最初の画像URLを正規表現で抜き、無ければ添付(enclosure)から画像を探してサムネイル候補にします。
本文からタグを剥がしたプレーンテキストの先頭300文字を一覧用の要約にします。
フィードIDは feed:42 のように feed: を頭に付けた文字列に統一しています。
これは後で「カテゴリは cat:、フィードは feed:」とプレフィックスで見分けるための地味だけど効く約束事です。

記事一覧は一度に全部取りに行かず、offset(何件目から)と limit(何件)で区切って取得します。
ArticleStore が「いま何件目まで読んだか(offset)」と「全体で何件あるか(totalCount)」を保持していて、hasContinuation()offset < totalCount で「まだ続きがあるか」を判定します。
一覧を下までスクロールすると「もっと読み込む」が走り、続きをAPIで追加取得して offset を進める、という形です。

詳細②:賢いサムネイル表示(4段階フォールバック)

RSSは配信元によって画像情報の入り方がまちまちで、素直に受け取るだけだと一覧が文字だらけになります。
そこで、サムネイルは src/services/thumbnail-resolver.ts で4段階のフォールバックを踏むようにしました。

  1. Minifluxの記事データに含まれる画像(article.imageUrl)があれば、それを即座に表示します。
  2. 無ければ、記事ページ本体を取得して og:image(SNSでシェアしたときに出る代表画像の指定)をバックグラウンドで拾い、見つかった時点でDOMを差し替えます。
  3. それも取れなければ、Googleのfavicon APIでサイトアイコンを即座にフォールバック表示します。
  4. 最終手段として、フィード名の頭文字をカラー背景に置いたアバターを表示します。

上に行くほど速く、下に行くほど確実、という並びです。
②の og:image 取得の実装がこれです。

private async fetchOgImage(url: string): Promise<string | null> {
    const response = await requestUrl({
        url,
        method: 'GET',
        headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ObsidianRSS/1.0)' },
    });
    const html = response.text;

    const ogMatch =
        html.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) ||
        html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i);
    if (ogMatch?.[1]) return ogMatch[1];

    const twMatch =
        html.match(/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i) ||
        html.match(/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']twitter:image["']/i);
    if (twMatch?.[1]) return twMatch[1];

    return null;
}

ポイントは2つあります。
1つ目は、propertycontent が逆順で書かれたタグにも備えて、正規表現を2パターン用意していることです。
サイトによって <meta property="og:image" content="..."> だったり <meta content="..." property="og:image"> だったりするので、片方だけだと取りこぼします。
2つ目は、og:image が無ければ twitter:image も試す二段構えにしている点です。

この取得はページを丸ごとダウンロードするので、無秩序にやるとサーバーに負荷をかけます。
そこで同時実行は最大5本まで、1本終わるごとに300ミリ秒待つというキューで流量を絞っています(MAX_CONCURRENT = 5FETCH_DELAY_MS = 300)。
一度解決した画像は設定ファイル(data.json)に7日間キャッシュするので、次回からは即表示です。
画像が無かった場合も「試したが無い」と記録するので、毎回同じページを取りに行く無駄も防いでいます。

なぜここまでやるかというと、1枚目で諦めると半分以上のカードが画像なしになり、一覧の見栄えが一気に崩れるからです。
ちなみに中身のない1×1ピクセルのスペーサー画像(URLに 1x1pixelspacer を含むもの)は、潰れた点が出てしまうので自動でスキップしています。

詳細③:気になった記事をその場でメモにする(クリッピング)

Obsidianを選んだ理由そのものの機能です。
記事は src/services/markdown-saver.tsMarkdownSaver でObsidianノートとして保存できます。
保存すると、フロントマター(元URL・ソース・著者・日付・スター状態・カテゴリのタグ)と、本文をMarkdownに変換したもの、それに「メモ」欄が付いたノートができます。

本文の変換は、外部ライブラリを使わず、HTMLタグを正規表現で素朴に置換しています(<h2>##<strong>**...**<a href>[...](...) など)。
完璧なHTMLパーサーではありませんが、RSS本文の変換にはこれで十分という割り切りです。
設定で「スターを付けたら自動保存」にもできるので、スター=あとで読む+ノート化という運用にできます。
これで読んだニュースが流れて消えず、自分の知識ベースの一部になります。

詳細④:AIニュース秘書はどう動くか(サーバー側の生成)

ここからが、このプラグインのもうひとつの顔であるAIキュレーションです。

仕組みを誤解されないように先に整理すると、AIの処理はプラグイン本体ではなく、自宅サーバー側のシェルスクリプトが担当しています。
プラグインは、そのスクリプトが書き出した結果を読んで表示する係です。
役割を分けたのは、AIの呼び出しをサーバーのcron(決まった時刻にコマンドを自動実行する仕組み)に任せ、プラグインは「読む」に専念させたかったからです。

サーバー側のスクリプト(server/rss-summarize.sh)の流れはこうです。

  • 未読ニュースを集める:MinifluxのAPIに「まだ読んでいない記事を一覧でちょうだい」と問い合わせます。実際には GET /v1/entries?status=unread&limit=...&order=published_at&direction=desc を叩き、必要なら after=(Unixタイムスタンプ)で「いつ以降の記事か」を絞ります。朝の回は前の晩からの分、日中の回は直近数時間の分、という具合に取りこぼしと重複を防ぎます。
  • 記事を整形する:集めたJSONをPythonでタイトル・URL・フィード名・本文(HTMLタグを剥がして800文字まで)のテキストに整えます。AIに渡す前の下ごしらえです。
  • AIに要約させる:整形したテキストを、コマンドで一度だけ呼び出すClaude(claude -p、Claude Codeのプロンプト実行モード)にパイプで渡します。

実際にAIへ渡しているプロンプトは、スクリプト内にそのまま書いてあります。
朝の要約はこうです。

prompt="以下は今朝までのニュース記事です。各記事を3行で要約してください。

形式:
### 記事タイトル
要約(3行程度)
[元記事](URL)

---

最後に「今朝のハイライト」として最も重要なニュース3つを1行ずつ挙げてください。"

echo "$articles_text" | claude -p "$prompt" >> "$output_file"  # 整形した記事の束をパイプで渡し、結果を追記

日本語で言うと、「整形した記事の束を claude -p に標準入力で流し込み、決まった見出し形式(### タイトル + 3行要約 + 元記事リンク)で要約させて、フロントマター付きのMarkdownファイルに追記する」だけです。

入力は未読記事のテキスト、出力はそのMarkdownです。
要約のような軽い作業なので、特別なAPIキーの管理もモデルの呼び分けもせず、サーバーに入っているClaude Codeをそのまま使っています。
category(カテゴリ指定の要約)・manual(直近N時間の手動要約)といったモードも同じ仕組みで、プロンプトと取得範囲だけを差し替えています。

結果のMarkdownはObsidianのボルト内に書き出され、Syncthing(自宅サーバーで動かしているファイル同期ツール)で全端末に配られます。
これで、スマホでも同じ内容を読めます。

詳細⑤:キュレーションの読み取り(パースが一番の難所)

サーバーが書き出したMarkdownを、プラグイン側の src/services/curation-loader.ts にある CurationLoader が読み取って、構造化したデータに組み直します。
プラグインが期待している実際の書式は、各ブロックの先頭にHTMLコメントの目印が付いた、こういう形です。

<!-- rss-curate: 2026-06-13-09 -->

## 09:00 ニュースキュレーション

### 必読 5件

### 1. 記事タイトル
**なぜ読むべきか:** 理由文
**3行要約:**
- 要約1
- 要約2
- 要約3
[元記事](URL)

### その他のヘッドライン

**カテゴリ名**
- [ ] [タイトル](URL) — フィード名

<!-- rss-curate: YYYY-MM-DD-HH --> という目印で、その日のどの時間帯(朝・9時・12時・15時・18時)の分かを判別します。
1つのデイリーノートに複数の時間帯が積み重なるので、まず目印の位置を全部拾って、隣り合う目印の間を1セクションとして切り出します。

このパースで一番神経を使ったのが、「必読」の各項目(### 1. ### 2. …)を1件ずつ切り出す正規表現です。

const itemRe = /###\s+(\d+)\.\s*([^\n]+)\n([\s\S]*?)(?=^###\s+\d+\.|^###\s*|^---\s*$|$(?![\s\S]))/gm;

日本語に翻訳すると、「### 数字. で始まる行をタイトルとして拾い、そこから次の項目(### 数字.)か、ヘッドラインの見出し(###)か、区切り線(---)か、文字列の最後にぶつかるまでを、その項目の本文とみなす」という意味です。

ここで一番のクセは末尾の $(?![\s\S]) です。
JavaScriptの正規表現には「文字列の本当の終わり」を表す \Z という記号が無いので、その代わりに「行末($)で、かつこの後ろに1文字も無い((?![\s\S]))」という条件で擬似的に表現しています。
これを忘れると、最後の項目(必読5件目)だけが終端マッチに失敗して取りこぼされるという、地味で気づきにくいバグになります。
実際に一度ハマってから入れた一行です。

切り出した本文からは、なぜ読むべきか: の後ろの理由、3行要約: の下の箇条書き、[元記事](URL) のリンクをそれぞれ別の正規表現で抜き出して、MustRead という型に詰めます。
ヘッドラインのほうは **カテゴリ名** の行でグループを区切り、その下の - [ ] [タイトル](URL) — フィード名 の行を1件ずつ拾っていきます。

こうして組み上がった構造を、CurationView が時間帯タブと必読カードのUIとして描き直します。
日付ナビで過去の分を遡ることもできて、その際はデイリーノートのフォルダを月単位でさかのぼって「キュレーションが含まれる日」を集めています。

詳細⑥:「いいね」をMinifluxのスターに橋渡しする

キュレーション画面の各記事には「いいね」ボタンが付いていて、押すとMiniflux側のスター(ブックマーク)がトグルされます。
読んで良かった記事に印を残せる、という機能です。

ここに、技術的にいちばん手間取った問題が隠れています。
キュレーションのMarkdownに載っているのは記事のURLだけで、スターを操作するのに必要なMinifluxの記事ID(entry ID)は分かりません。
しかも、RSSで配られるURLとMiniflux側に保存されているURLは、末尾のスラッシュやトラッキングパラメータ(utm_ などの計測用の付加情報)が微妙に違っていて、単純な文字列一致では引き当てられないのです。

そこで MinifluxClientfindEntriesByUrls() で、URLを2種類のキーで両引きする照合をしています。

for (const e of entries) {
    if (!e.url) continue;
    // 1) 正規化URL(小文字化・www.除去・トラッキングパラメータ除去・末尾スラッシュ除去)で照合
    const norm = normalizeUrl(e.url);
    let orig = normToOrig.get(norm);
    if (!orig) {
        // 2) それでもダメなら「ホスト+パスだけ」で再照合
        const pk = pathKey(e.url);
        orig = pathToOrig.get(pk);
    }
    if (orig && !map.has(orig)) {
        map.set(orig, { id: String(e.id), starred: e.starred });
    }
}

日本語にするとこうです。

まずキュレーション側とMiniflux側、両方のURLを正規化(https化・www. を剥がす・utm_ などの計測パラメータを落とす・末尾スラッシュを削る)して突き合わせます。
それでも一致しなければ、クエリやフラグメントを全部捨てて「ホスト名+パスだけ」にしたゆるいキーで再照合します。
Minifluxには「このURLの記事をくれ」という直接の検索APIが無いので、新しい順に最大5000件まで記事を取得しながら、見つかったものから埋めていく作りです。

これで「いいね」したキュレーション記事を、ちゃんとMiniflux側のスターと結びつけられます。
万一そのとき照合できなかった記事でも、いいねを押した瞬間に findEntryByUrlOrTitle() でURLとタイトルから再検索する保険を入れてあります。
こういう「現実のデータの揺れを吸収する」泥臭い処理は、地味ですが効きます。

詳細⑦:技術構成(モジュールとデータフロー)

中身を自分でも作ってみたい方のために、全体像を置いておきます。
土台は次の通りです。

技術構成(言語・土台・ビルド・通信・対応環境)

モジュールは「責務を分ける」ことを意識して、役割ごとに分けています。
実際のディレクトリ構成はこうです。

src/
├── main.ts                  プラグインエントリ(ビュー登録・自動更新)
├── api/
│   └── miniflux-client.ts   Miniflux REST APIクライアント(X-Auth-Token認証)
├── store/
│   └── article-store.ts     記事キャッシュ・フィルタ・状態管理(オブザーバー)
├── services/
│   ├── markdown-saver.ts    記事をMarkdownノートに保存
│   ├── thumbnail-resolver.ts サムネイル4段階フォールバック
│   └── curation-loader.ts   デイリーノートからキュレーションをパース
├── views/
│   ├── reader-view.ts       リーダー画面(Discover/Magazine/List)
│   ├── curation-view.ts     キュレーション画面
│   ├── browser-view.ts      Obsidianタブ内のiframeブラウザ
│   ├── sidebar.ts           共通サイドバー
│   └── settings-tab.ts      設定画面
├── types/                   ドメイン別の型定義(api / filter / settings / curation)
└── utils/                   日時・URL正規化などのユーティリティ

設計の中心は ArticleStore です。
これは取得した記事をメモリに持ち、フィルタ(未読のみ/スター/カテゴリ/キーワード検索)をかけ、状態が変わると登録済みの画面に「変わったよ」と通知するオブザーバーパターンで作っています。
UIと状態を分離してあるので、画面側は「ストアが変わったら描き直す」だけで済みます。

データの流れを実際の関数名で追うと、リーダー画面はこうです。

  • ReaderView が開くと ArticleStore.loadFiltered() を呼ぶ
  • それが MinifluxClient.getEntries() でAPIを叩き、parseEntry()Article 型に変換
  • ストアが notify() で変更を通知し、ReaderViewgetFiltered() でフィルタ済みの配列を受け取って renderDiscoverCard() などで描画

キュレーション画面はこうです。

  • CurationViewCurationLoader.loadByDate() を呼ぶ
  • デイリーノートを読んで parseSection()parseMustReads() / parseHeadlines() で構造化
  • 描画後にバックグラウンドで findEntriesByUrls() を回し、各記事にMinifluxのIDを後付けで紐付けて「いいね」を有効化

連打への備えも入れていて、フィルタを高速に切り替えても、loadFiltered() 内で発行した連番(loadSequence)と最新の連番を比べ、古いレスポンスが新しい表示を上書きしないようにしています。

動作の確認状況

正直に書くと、このプラグインには自動テストはまだ用意していません。
動作確認は、自宅サーバーのMiniflux(フィードはInoreaderから移行した「主要ニュース」「Gadget」などのカテゴリ、1日200〜380件程度の流量)に対して、PCとスマホのObsidianで実際に毎日読みながら確認しています。

記事一覧・既読同期・スター・Markdown保存・キュレーション表示・いいねのMiniflux反映までは、日々の利用で動いている状態です。
テストが無いぶん、前述の「割り切った仕様」やつまずいた箇所は、コード内のコメントに「なぜこうしたか」を書き残すようにしています。

導入と設定

必要なもの

  • Obsidian v1.0.0 以降(PC・スマホどちらでも可)
  • 自前のMinifluxサーバーと、そのAPIキー

このプラグインは「自分のMinifluxを読む」ための道具なので、先にMiniflux本体が必要です。
Minifluxの立て方は、自宅サーバーにMinifluxを立てた記事にまとめています。

設定はこれだけ

導入できたら、Obsidianの設定タブで入力するのは基本2つです。

設定項目(サーバーURLとAPIキー)

入力したら「接続テスト」ボタンで疎通を確認できます。
サーバーURLの末尾に余計なスラッシュや /v1 が付いていても、クライアント側で吸収するようにしてあるので、おおよそで大丈夫です。
外出先から使うときは、サーバーURLにTailscale(インターネットに公開せず端末同士をつなぐプライベートな仮想ネットワーク)のアドレスを入れておけば、外からそのまま読めます。

キュレーション画面を使う場合は、もう1項目、書き出し先のデイリーノートのフォルダを指定します({YYYY}{YYYY-MM} といったトークンが使えます)。
ここを、サーバーのスクリプトがキュレーションを書き込んでいる場所に合わせます。

CORSについての注意

PC版のObsidianアプリは app://obsidian.md という出所からリクエストを出します。
Miniflux側(やその前段のリバースプロキシ)で、この出所からのアクセスを受けられるようにしておく必要がある場合があります。
うまく繋がらないときは、ここを確認してみてください。

使い方

  • リボンの「すべてのニュース」(または「キュレーション」)アイコンを押す
  • リーダー画面では、左サイドバーでカテゴリやフィードを選び、上部でレイアウト(Discover / Magazine / List)を切り替える
  • 記事をクリックして本文を読み、s でスター、m で既読、v で元記事を開く
  • キュレーション画面では、時間帯タブを切り替えて「今日の必読」を読み、良かった記事に「いいね」を付ける

フィードの追加・削除はMiniflux本体のWeb UIで行います(このプラグインは閲覧専用です)。
「読むのはObsidian、購読管理はMiniflux」という分担です。

自宅サーバーの仕組みとのつながり

このプラグインは、自宅サーバーで作ってきた仕組みの最後の「読む」を担当しています。

  • Minifluxの記事:プラグインがAPIで記事を読み出す元です。RSSの無いサイトも、RSSHub(任意のサイトのRSSを生成するツール)をサーバーで動かしてMiniflux側に集約してあります。
  • キュレーションの生成:サーバー側のスクリプト(cron+Miniflux API+claude -p)が「必読+ヘッドライン」を書き出し、それをこのプラグインのキュレーション画面が読み取って描画します。サーバー側で書き出す書式を変えたら、プラグイン側の読み取り(curation-loader.ts の正規表現)も合わせて直す、という形式レベルで噛み合った関係です。
  • Syncthingの記事:ボルト(書き出されたキュレーションを含む)を全端末に同期しているので、スマホでも同じ画面・同じ内容で読めます。

つまり、「Minifluxが集める → AIが選んで要約する → Syncthingが全端末に配る → このプラグインがObsidianで読む(そしてメモ化する)」という一本の流れが、ここで完成します。

よくあるトラブルとQ&A

Q. 「接続エラー 401」と出る

A. APIキーが間違っているか、空のことが多いです。
Minifluxの「設定 → APIキー」で発行した値を貼り直してください。
401のときは「ユーザー名またはパスワードが違います」というヒントも一緒に表示されます。

Q. 接続テストは通るのに、記事が表示されない

A. そのカテゴリ/フィードに未読が無いか、フィルタが「未読のみ」になっている可能性があります。
ヘッダーの「未読のみ/すべて」トグルで「すべて」に切り替えるか、Miniflux本体でフィードがちゃんと記事を取得できているかを確認してください。

Q. PCでうまく繋がらない(接続テストも失敗する)

A. 前述のCORSのケースが考えられます。
Miniflux(やリバースプロキシ)が、Obsidianアプリの出所(app://obsidian.md)からのリクエストを受けられる設定か確認してください。

Q. サムネイルが頭文字アバターばかりになる

A. そのフィードの記事に画像情報が乏しく、og:image も取得できていない状態です。
Miniflux側でそのフィードの「元記事の全文取得(Fetch original content)」をオンにすると、記事固有の画像が拾えるようになることがあります。

Q. キュレーション画面が空になる

A. 設定の「書き出し先のフォルダ」が、サーバーのスクリプトが書き込んでいる場所と一致しているかを確認してください。
その日のキュレーションがまだ生成されていない場合も空になります(初回表示時は、直近の生成済みの日に自動で寄せる作りにはしています)。

Q. 「いいね」を押しても反映されない

A. キュレーションの記事URLとMiniflux側の記事をURLで照合する仕組みのため、URLが大きく食い違うと紐付けに失敗することがあります。
その場合は「Miniflux でこの記事を見つけられませんでした」と通知が出ます。
Miniflux側にその記事がまだ取り込まれていないケースが多いです。

まとめ

いかがでしたか。
今回は、Inoreaderをやめて、ObsidianをRSSリーダー兼AIニュース秘書にする自作プラグイン Miniflux Reader を、内部のコードまで開けて紹介しました。
要点を振り返ります。

  • 有料のクラウドRSSをやめ、自前のMinifluxの記事を、Inoreader並みの読み心地(3レイアウト・4段階サムネイル・キーボード操作)でObsidianの中に持ってきた
  • APIクライアント・記事ストア・サービス層・ビューに責務を分け、requestUrl でCORSを回避しつつMinifluxと既読・スターを同期した
  • AIニュース秘書は、サーバー側の claude -p が未読を選んで要約し、プラグインが決まった書式のMarkdownをパースして表示する分担。記事URLとMinifluxの記事を2段の正規化で照合し、「いいね」をスターに橋渡しした

情報のインプットからメモ化までをObsidianの中で完結できるようになり、ニュースを「読んで終わり」にせず自分の知識につなげやすくなりました。

このプラグインは自分用に作ったものですが、同じように自宅サーバーでMinifluxを使っている方には役立つかと思います。
この記事の構成やコード抜粋を、ChatGPTやClaudeに渡して自分用の改造版を作ってもらうのも面白いと思います。

コード一式を使ってみたい・中身を見てみたいという方は、コメントでお知らせいただければお渡しします。

この記事が誰かの役に立てばうれしいです。