概要
皆さんこんにちは。今回は「n8n」を使ってニュース記事を自動収集し、収集した結果をGitHubに集積し、ローカル側でObsidianと言うノートアプリで参照する仕組みを作りましたので、その設定内容をご紹介します。
内容はありきたりなんですが、なんでも自分でやってみる事が大事だと思っています!

n8n(エヌエイトエヌ)は、Slack、Gmail、Googleシートなど400以上のアプリを、ノード(ブロック)を繋ぐ直感的な操作(ノーコード/ローコード)で連携し、業務を自動化できる強力なワークフロー自動化ツールです。最大の特徴はオープンソースであり、自身のサーバー環境(セルフホスト)で運用すれば無料かつ無制限に自動化が可能な点です。
GitHub(ギットハブ)は、世界中の開発者がプログラムのソースコードをオンライン上で管理・共有・共同作業できる、世界最大級のソフトウェア開発プラットフォームです。Gitという技術を基盤に、コードの変更履歴保存、バグ管理、コードレビューを支援し、個人から企業まで幅広く利用されています。
Obsidian(オブシディアン)は、ローカル環境(PC・スマホ)で動作する、マークダウン形式の強力なナレッジベース・ノートアプリです。最大の特徴は、メモ同士を双方向リンクでつなぎ、知識のネットワークを可視化(グラフビュー)できる点にあります。基本機能は無料かつ高度なカスタマイズが可能で、アイデア整理や長期的な知識管理に最適です。
Obsidianでのニュースの見え方、設定内容
Obsidianでのニュースの見え方

毎朝Obsidianを起動すると、日付フォルダの中に、自分が設定したカテゴリ毎に収集したニュースファイルが表示されます!
そして記事タイトルがリンクになっており、記事本文にジャンプします。
本当は記事本文を要約した結果を収集したかったのですが、Google RSS Readerでは記事本体のリンクに辿り着く事ができず、断念しました。Yahoo RSS Readerだと記事本体へのリンクが取得できるようで、この後そちらも試してみようと思います。
Obsidianの設定
ニュースを見た際にmdファイルが変更された状態となり、次のPullタイミングで先にcommitかstashしろ!となってしまいます。問題は恐らくWindowsとLinuxでの改行コードの違いにあると思いますので、.gitattributesファイルを作り、以下の内容を記述しておきます。
text=auto eol=lf
*.md text eol=lf
ついでに.gitignoreファイルは以下の内容になります。
# to exclude vault's settings (including plugin and hotkey configurations)
.obsidian/
# OR only to exclude workspace cache
.obsidian/workspace.json
.obsidian/workspace-mobile.json
# Add below lines to exclude OS settings and caches
.trash/
.DS_Store
Obsidian、プラグインの設定

Obsidianにはプラグインを自由にインストールできるようになっており、VinzentさんのGitを使用しています。設定は以下の通り。
ネットで検索した記事にはどれも「Obsidian Git」をインストールする、と言う説明になっていましたが、「Obsidian Git」と言う名称のプラグインは無く、開発者を見る限り、上記の「Git」プラグインだと思います。
- Auto pull interval (5mins)
- Pull on startup (ON)
- Merge strategy on conflics (Their changes)
Obsidianの初期起動時に、GitHubに収集したニュース記事ファイルをクローンしたフォルダを、vaultフォルダとして指定します。
n8nの設定
n8nサービス
n8nは、ワークフローを設定するGUIを伴うWebサービスです。現状でこのサービスを利用するには、n8n cloudを使うかセルフホストさせるかの2択になります。
今回は、少し前に構築したUbuntu Server 24.04の社内環境にDockerにて構築しました。以下、dockerのcomposeファイルです。コメントが残っていて見苦しい限りですが、ご容赦ください。
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n-1
restart: always
user: "1000:1000" # Dockerの実行ユーザーがvolumsにアクセスできるようになる。
ports:
- "5678:5678"
environment:
- TZ=Asia/Tokyo # timezoneは、ワークフロー内の実行時間をJSTに揃える
- GENERIC_TIMEZONE=Asia/Tokyo
- NODE_TZ=Asia/Tokyo
- EXECUTIONS_MODE=regular
#- N8N_RUNNERS_ENABLED=false
#- N8N_LOG_LEVEL=debug
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
#- N8N_PROTOCOL=http https 前提時に必要 http直アクセスなら不要
#- N8N_HOST=192.168.xxx.xxx 外部公開URLに指定 LAN内アクセスは自動判定でOK
- N8N_SECURE_COOKIE=false #https時のcookie制御 httpなら関係ない → やっぱり必要
#- WEBHOOK_URL=http://192.168.xxx.xxx:5678/ 外部からWebhookを受ける時に必要 RSS収集だけなら不要
volumes:
- ./n8n_data:/home/node/.n8n
これで実行回数制限なし、無料で使える安心環境の出来上がりです!Docker最高!
ワークフローの設定
つづいてワークフローの設定です。ブラウザでアクセスし、ユーザー登録する部分は割愛します。

以下、簡単な処理の流れ、および設定内容を説明します。
Schedule Triggerノード。
毎朝8時(JST)に起動するように設定しています。そして、フローが出来上がったら右上の「Publish」で全体を有効化します。
設定内容は想像通りで超簡単なのに、時間が来ても一向に実行されなくて少し嵌りましたが、「Publish」状態にしないと実行されませんのでご注意ください。(フローを変更するとUnpublished状態に戻るので、その際も忘れずに。)
Code in Javascript + GitHubノード。
後述しますが、今回GitHubに記事ファイルを蓄積しますが、リポジトリ名は「vault」、その中に「news/yyyy-mm-dd/記事のカテゴリ名.md」と言うファイル名で保存します。安定稼働すれば問題ないと思いますが、GitHubのCreate a fileノードは同名ファイルをアップするとエラーになる為、最初に当日ファイルはクリアしておくようにしました。
と言う事で、Javascriptで当日の日付文字列を生成し、GitHubの上記リポジトリ「vault」を参照、中のファイルリストを取得します。そして取得したリストを1ファイルずつループし、ファイルを削除していきます。

以下、loop時のノードの繋ぎ方です。

ファイル取得ノードのError側、ループノードのdone側を後続のカテゴリ設定ノードに繋ぎます。
Code in Javascriptノード。
以下のコードで、カテゴリ名と日付文字列(後にファイル名として使用)、検索キーワードを設定しています。後続のloopノードでは、以下の項目で1カテゴリ毎に処理を行います。{“category”, “url”, “dateStr”}
GoogleのRSS Readerはクエリーが使えるので、任意の検索キーワードを設定します。スペース繋ぎの複数キーワードはAND条件と解釈されるので、OR条件にしたい場合は明示的に「OR」(確か大文字)を記述します。
const nowJst = new Date(
new Date().toLocaleString("en-US", { timeZone: "Asia/Tokyo" })
);
const yyyy = nowJst.getFullYear();
const mm = String(nowJst.getMonth() + 1).padStart(2, '0');
const dd = String(nowJst.getDate()).padStart(2, '0');
const dateStr = `${yyyy}-${mm}-${dd}`;
const queries = [
{ category: '1.*****', query: '*****' },
{ category: '2.国内政治経済', query: '国内 AND (政治 OR 経済)' },
{ category: '3.海外政治経済', query: '海外 AND (政治 OR 経済)' },
{ category: '4.*****・暮らし', query: '***** AND 暮らし' },
{ category: '5.IT技術動向', query: 'IT技術 OR Python OR C# OR PHP OR Javascript OR TypeScript OR Node.js OR ***' },
{ category: '6.スポーツ・芸能', query: 'スポーツ OR 芸能' },
{ category: '7.*****', query: '*****' },
{ category: '8.*****', query: '*****' },
];
return queries.map(q => ({
json: {
category: q.category,
url: `https://news.google.com/rss/search?q=${encodeURIComponent(q.query)}&hl=ja&gl=JP&ceid=JP:ja`,
dateStr: dateStr
}
}));
RSS ReadのインプットになるのはSTEP3で設定した、3項目のJSON文字列です。その中の「url」を、RSS ReadのURL欄に設定します。
{“category”, “url”, “dateStr”}

RSS Readの結果は以下項目のJSON文字列が返ってきます。これはGoogle RSS Readerの仕様で固定されており変更できません。
{“title”, “link”, “pubDate”, “content”, “contentSnippet”, “guid”, “isoDate”}
これは、後続処理で使いたかった「category」や「dateStr」の項目が、RSS Readノードを通過する事で無くなってしまいます。カテゴリと日付はmdファイル名に使いたかったので、無くなって貰っては困るんです。
その解決方法を僕は見つけました!Edit Fieldsノードで事前に設定したカテゴリ名称などを出力列に追加する事ができるんです!ChatGPT君もよく分かっていなくてかなりハマりました。

「$(‘ノード名’).item.json.xxx」で直前より前のノードの値を取得できますので、直前ノードの出力項目にcategory、dateStrを追加した以下のJSONを、後続のノードに渡します。
{“category”, “dateStr”, “title”, “pubDate”, “link”}
続いてCode in javascriptノードで、フィルタ&ソートを実行。
const items = $input.all();
// JST 現在時刻
const now = new Date();
const jstNow = new Date(now.getTime() + 9 * 60 * 60 * 1000);
// 48時間前
const past48h = new Date(jstNow.getTime() - 48 * 60 * 60 * 1000);
// フィルタ
const filtered = items.filter(item => {
const pub = new Date(item.json.pubDate);
const pubJst = new Date(pub.getTime() + 9 * 60 * 60 * 1000);
return pubJst >= past48h;
});
// ★ ここが追加:pubDateで降順ソート
filtered.sort((a, b) => {
const timeA = new Date(a.json.pubDate).getTime();
const timeB = new Date(b.json.pubDate).getTime();
return timeB - timeA; // 新しい順
});
return filtered;
RSS Readの結果は日付順に並んでいなかったので、自前でソートと過去の参照範囲を設定します。(1回のリクエストで、古いの含めて100件ほどニュースが返ってきます。)
RSS Readは、1リクエストで100件ほど返って来るので、20件に調整して本ノード(Code in javascript)で、20ニュース記事を1mdファイルにまとめます。インプットJSONは、STEP4で記載した以下の形です。
{“category”, “dateStr”, “title”, “pubDate”, “link”}
const items = $input.all();
if (items.length === 0) return [];
// ★ ここで今日の日付を作る(JST)
const now = new Date();
const jst = new Date(now.getTime() + 9 * 60 * 60 * 1000);
const yyyy = jst.getFullYear();
const mm = String(jst.getMonth() + 1).padStart(2, '0');
const dd = String(jst.getDate()).padStart(2, '0');
const folderDate = `${yyyy}-${mm}-${dd}`;
const { category } = items[0].json;
let md = `tags: [${category}]\ndate: ${folderDate}\n\n`;
md += `# ${category}ニュース(${folderDate})\n\n---\n\n`;
items.forEach((item, index) => {
const { title, link, pubDate } = item.json;
md += `### ${index + 1}. [${title}](${link})\n`;
md += `> ${pubDate}\n\n---\n\n`;
});
const filename = `news/${folderDate}/${category}.md`;
return [{ json: { filename, md } }];
このcodeでfilenameとmd本文を生成しています。{“filename”, “md”}
ファイル名は「news/日付/カテゴリ名.md」と言う形にしており、このファイル名でGitHubにファイルを作成する事で階層構造(フォルダ構成)を作る事ができます。
GitHubにPushされたファイル
GitHubにPushされたファイルを見ていきます。

基本的にObsidianの見た目と同じですね。
リポジトリは、これ専用に「vault」と言う名称で作成しました。
そしてアップするファイル名が、「news/yyyy-mm-dd/カテゴリ名.md」ファイルです。ファイル名に区切り文字を入れる事で、GitHub上でもObsidian上でもフォルダとして認識されます。
GitHubでファイルを蓄積する意味は、無料でファイルサーバー的に使用できる事、また見る側のPCやスマホは、常にGitHubからファイルを取得しますので、どこからでも同じ情報を見る事が可能になります。こうやってGitHubを利用するの、他でもありそうですね。
AndroidスマホでのObisidian
表示のされ方

スマホの設定、分かりにくいんですが、正しく設定するとこんな感じで表示されます。
PC用のObsidianと、同じイメージで表示できますね。
設定方法

画面下部の歯車マーク > Settings > Community plugins > Gitをインストールします。PC版と同じくVinzentさんのものです。
設定する項目は以下になります。Auto Pullの設定とか、見当たらず。一応これで同期できたのですが、Custom base pathを未設定にするとできなくなるのか等、詳細まで踏み込んでいません。
- Username on your git server
- Password/Personal access token
- Custom base path (Git repository path)
同期方法

新しいタブを開き、画面上部からプルダウンすると、コマンドパレットが表示されます。以下の順番に入力します。
- Git: Clone an existing remote repo
- Enter remote URL → リポジトリのURL
- Enter directory for clone → ローカル側のクローンするディレクトリ名。任意の名称でOK。
- Specify depth of close → 未入力
設定含めて正しく設定できていると、ここで同期が走り、ファイルがPullされます。
残課題
一通り動く状態となり、毎日ニュース記事を収集してくれていますが、今後は以下の項目を対応したいと考えています。
- GitHubに蓄積する内容を記事本文にしたい。 (現状、タイトル文字列とリンク、日付など。)
- またその際、AIを用いて要約された状態で蓄積したい。 (現状、AIは未使用。)
- Obsidianのメモ同士を繋ぐ機能、グラフ機能などを活用したい。(現状、単なるテキスト表示アプリでしかない。)
以上です。
見るのは簡単なので、設定方法を中心とした解説記事となりました。残課題に取り組んだら、また報告させていただきます。
n8nは直感的にノードを繋いでいけるので簡単そうなんですが、出力側がSuccessとErrorに分かれている物、Loopを使った場合はDone側とLoop側をどう繋ぐのか、ちゃんと分かっていないと流れません。難しくはないのですが、少しだけ慣れが必要でしたね。
最後に、手動実行した際の進み方を見てみましょう。きちんとループしているのも分かります。