RSS リーダー
バックグラウンドサーバースクリプトで外部の RSS / Atom フィードを定期取得し、items.Upsert で記事テーブルに自動登録すると、プリザンターを RSS リーダーとして使えます。 記事のリンク URL を Upsert のキーにするため、同じ記事が重複登録されることはありません。取り込んだ記事には、プリザンターのフィルタ・ソート・通知機能をそのまま使えます。
逆向き(プリザンターからフィードを配信する)は標準機能に無い
このページは外部のフィードを取り込む方法です。プリザンターのサイトの新着・更新を RSS / Atom として配信する機能は 1.5.8.1 にはありません。本体を改修して配信する場合の設計は RSS / Atom フィード配信の設計 にまとめています。
処理の流れ
図を読み込み中…
- 設定した各フィード URL に
httpClient.Get()で XML を取得する - XML から記事のタイトル・リンク・概要・公開日時を正規表現で抽出する
- リンク URL をキーに
items.Upsertで記事テーブルに登録・更新する
前提条件
Script.json
App_Data/Parameters/Script.json の BackgroundServerScript を true にします(既定値は false です。Script.json)。
{
"ServerScript": true,
"BackgroundServerScript": true
}WARNING
パラメータファイルの変更後はプリザンターの再起動が必要です。パラメータ再読み込み機能を使う場合は、特権ユーザでログインして実行してください。
記事格納用テーブル
期限付きテーブルを作成し、サイト ID を控えておきます。
| カラム | 用途 | 説明 |
|---|---|---|
| タイトル | 記事タイトル | フィードの <title> |
| 内容 | 記事概要 | フィードの <description> や <summary> |
| 分類A | リンク URL | Upsert のキーとして使用 |
| 分類B | フィード名 | どのフィードから取得したかを識別 |
日付A(DateA) | 公開日時 | フィードの <pubDate> や <published>。スクリプトではリクエストの最上位に DateA として渡す |
分類 C 以降を使っても構いません。
XML のパース
サーバースクリプトは JavaScript(V8 / ClearScript)で動作しますが、DOMParser のようなブラウザ API は使えません。そのため XML は正規表現でパースします。RSS と Atom はタグ名が異なるため、両方に対応します。
| 関数 | 役割 |
|---|---|
getTagValue | タグの中身を取得する。CDATA セクション(<![CDATA[...]]>)にも対応 |
getHref | Atom の <link href="..."> から URL を取得する |
parseEntries | <feed タグが含まれていれば Atom、それ以外は RSS 2.0 として記事の配列を返す |
WARNING
この正規表現ベースのパーサは一般的な RSS 2.0 / Atom フィードに対応しますが、名前空間付きタグ(例: <dc:creator>)や複雑な構造には対応していません。対象フィードに合わせてカスタマイズしてください。
バックグラウンドサーバースクリプト
登録手順
- 特権ユーザでログインする
- 「テナント管理」→「サーバスクリプト」タブを開く
- 「新規作成」をクリックする
- 次を設定して「追加」→「更新」
| 項目 | 設定値 |
|---|---|
| タイトル | RSSフィード取得 |
| 条件 | バックグラウンドサーバスクリプト |
| スケジュール | 毎時(任意) |
| 無効 | チェックなし |
| サーバスクリプト | 下記 |
スクリプト
// --- 設定 ---
var targetSiteId = 12345; // 記事格納テーブルのサイトID
var feeds = [
{ name: 'Qiita - Pleasanter', url: 'https://qiita.com/tags/pleasanter/feed' },
{ name: 'Qiita - プリザンター', url: 'https://qiita.com/tags/プリザンター/feed' }
];
// --- 設定ここまで ---
feeds.forEach(function (feed) {
try {
// フィードを取得
httpClient.RequestUri = feed.url;
var response = httpClient.Get();
if (!httpClient.IsSuccess) {
logs.LogInfo('フィード取得に失敗: ' + feed.name + ' (' + feed.url + ')');
return;
}
// XMLをパースして記事を抽出
var entries = parseEntries(response);
if (entries.length === 0) {
logs.LogInfo('記事なし: ' + feed.name);
return;
}
var upsertCount = 0;
entries.forEach(function (entry) {
try {
if (!entry.link) return;
var data = {
Keys: ['ClassA'],
Title: entry.title || '(無題)',
Body: entry.summary || '',
ClassA: entry.link,
ClassB: feed.name
};
// 公開日時をセット
if (entry.pubDate) {
var d = new Date(entry.pubDate);
if (!isNaN(d.getTime())) {
data.DateA = d.toISOString();
}
}
var result = items.Upsert(targetSiteId, JSON.stringify(data));
if (result) {
upsertCount++;
}
} catch (entryError) {
logs.LogInfo('エントリ処理エラー: ' + entry.link + ' - ' + entryError.message);
}
});
logs.LogInfo(
'フィード処理完了: ' + feed.name
+ ', 取得件数=' + entries.length
+ ', Upsert件数=' + upsertCount
);
} catch (feedError) {
logs.LogInfo('フィード処理エラー: ' + feed.name + ' - ' + feedError.message);
}
});
// --- パース関数 ---
// XMLテキストからタグの値を取得する
function getTagValue(xml, tagName) {
var pattern = new RegExp(
'<' + tagName + '[^>]*>([\\s\\S]*?)</' + tagName + '>'
);
var match = xml.match(pattern);
return match ? match[1].replace(/<!\[CDATA\[|\]\]>/g, '').trim() : '';
}
// XMLテキストからhref属性を取得する(Atom用)
function getHref(xml, tagName) {
var pattern = new RegExp('<' + tagName + '[^>]*href="([^"]*)"');
var match = xml.match(pattern);
return match ? match[1] : '';
}
// RSS/Atom XMLをパースして記事の配列を返す
function parseEntries(xml) {
var entries = [];
if (xml.indexOf('<feed') !== -1) {
// Atom フィード
var entryBlocks = xml.split(/<entry[\s>]/);
for (var i = 1; i < entryBlocks.length; i++) {
var block = entryBlocks[i];
entries.push({
title: getTagValue(block, 'title'),
link: getHref(block, 'link'),
summary: getTagValue(block, 'summary')
|| getTagValue(block, 'content'),
pubDate: getTagValue(block, 'published')
|| getTagValue(block, 'updated')
});
}
} else {
// RSS 2.0 フィード
var itemBlocks = xml.split(/<item[\s>]/);
for (var j = 1; j < itemBlocks.length; j++) {
var itemBlock = itemBlocks[j];
entries.push({
title: getTagValue(itemBlock, 'title'),
link: getTagValue(itemBlock, 'link'),
summary: getTagValue(itemBlock, 'description'),
pubDate: getTagValue(itemBlock, 'pubDate')
});
}
}
return entries;
}| 設定項目 | 説明 |
|---|---|
targetSiteId | 記事を格納するテーブルのサイト ID |
feeds | 取得するフィードの配列。name はフィード名(分類B に格納)、url はフィードの URL |
Keys: ['ClassA'] でリンク URL(分類A)をキーに指定しているため、同じ URL の記事は更新、新しい URL の記事は新規作成されます。
公開日時は DateHash を使わず、最上位の DateA に入れています。API のモデルは ClassA や DateA を最上位のプロパティとしても受け付けます(_BaseApiModel.cs)。
httpClient.ResponseHeaders は Get() などの呼び出しごとに内部でクリアされる(ServerScriptModelHttpClient.cs)ため、事前に Clear() する必要はありません。
フィード取得の成否や Upsert 件数は logs.LogInfo で SysLogs に記録します(ServerScriptModelLogs.cs)。context.Log の出力は画面へ返す応答(ブラウザのコンソール)に載るだけなので、画面を持たないバックグラウンドサーバースクリプトでは残らないため、logs.LogInfo に修正しています。
フィードの追加
feeds 配列にオブジェクトを追加するだけです。
var feeds = [
{ name: 'Qiita - Pleasanter', url: 'https://qiita.com/tags/pleasanter/feed' },
{ name: 'Qiita - プリザンター', url: 'https://qiita.com/tags/プリザンター/feed' },
{ name: 'Zenn - Pleasanter', url: 'https://zenn.dev/topics/pleasanter/feed' }
];新着記事の通知
items.Upsert でレコードが作成・更新されると、テーブルに設定された通知が発動します。記事格納テーブルの「テーブルの管理」→「通知」タブで通知を追加するだけで、新着記事をメールやチャットで受け取れます。
| 項目 | 設定値 |
|---|---|
| 通知種別 | メール / HttpClient など |
| 条件 | 作成後 |
| アドレス | 通知先のメールアドレスや Webhook URL |
「作成後」を条件にすると、新しい記事が初めて登録されたときだけ通知されます。同じ記事が更新された場合も通知したい場合は「更新後」も追加します。
カスタマイズ例
HTML タグの除去
概要に HTML タグが含まれる場合は、除去してから Body に格納します。
function stripHtml(html) {
return html.replace(/<[^>]*>/g, '');
}
data.Body = stripHtml(entry.summary || '');古い記事の自動削除
同じバックグラウンドサーバースクリプト内で、items.BulkDelete を使って一定期間(例: 90 日)より古いレコードをまとめて削除できます。
// --- 古い記事の削除(90日以上前) ---
var maxDays = 90;
var cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - maxDays);
var cutoff = cutoffDate.getFullYear() + '/'
+ (cutoffDate.getMonth() + 1) + '/'
+ cutoffDate.getDate();
// 日付の範囲は「開始,終了」。開始を空にすると「終了以前」になる
var deleted = items.BulkDelete(targetSiteId, JSON.stringify({
View: {
ColumnFilterHash: {
DateA: '[",' + cutoff + '"]'
}
}
}));
if (deleted > 0) {
logs.LogInfo('古い記事を削除: ' + deleted + '件');
}日付項目のフィルタは 開始,終了 の形式で、終了を空にすると「開始以後」、開始を空にすると「終了以前」になります(View.cs)。'["' + 日付 + ',"]' と書くと基準日より新しい記事が対象になるため、開始を空にする形に修正しています。
また、httpClient から context.ApplicationPath + 'api/...' を呼ぶ方法は、ApplicationPath がホスト名を含まないパスのため URL として成り立ちません。サーバースクリプトの items.BulkDelete(サイトID, JSON)(ServerScriptModelApiItems.cs)を使えば API キーも不要なので、こちらに修正しています。
WARNING
items.BulkDelete は、バックグラウンドサーバースクリプトの実行ユーザーの権限で動きます。実行ユーザーが記事テーブルの削除権限を持っている必要があります。