RSS / Atom フィード配信の設計
プリザンターには、サイトのレコードを RSS / Atom で配信する機能はありません(外部のフィードを取り込む方法は RSS リーダー を参照)。このページは、サイトの新着・更新をフィードとして公開する本体改修の設計メモです。
推測できない URL、実行ユーザーの Context、ビューの適用、[列名] のテンプレートは iCal フィードの設計 と同じ考え方で、前提にした現行実装(1.5.8.1 のフォーム機能・バックグラウンドサーバースクリプト・SiteSettings.IncludedColumns)もそちらに書いています。ここでは差分を中心にまとめます。
要件
| 要件 | 内容 |
|---|---|
| サイト単位・推測できない URL | iCal と同じ |
| 形式 | RSS 2.0 と Atom 1.0 の両方 |
| 実行ユーザー・ビュー・複数設定 | iCal と同じ |
| タイトル・内容の書式 | 標準(画面表示タイトル・内容)か [列名] テンプレート |
| 件数制限 | フィードに含める件数の上限を設定できる |
RSS 2.0 と Atom 1.0
| 観点 | RSS 2.0 | Atom 1.0(RFC 4287) |
|---|---|---|
| 構造 | <rss><channel><item> | <feed><entry> |
| 日時 | RFC 822 | RFC 3339 |
| エントリの識別子 | <guid>(任意) | <id>(必須) |
| 更新日時 | <pubDate>(任意) | <updated>(必須) |
| Content-Type | application/rss+xml | application/atom+xml |
Atom はフィード全体の <title>・<id>・<updated> が必須で、エントリに <author> が無いときはフィード側に <author> が必要です。
生成ライブラリ
.NET の NuGet パッケージ System.ServiceModel.Syndication に、共通のデータ構造 SyndicationFeed / SyndicationItem と、書き出し用の Rss20FeedFormatter / Atom10FeedFormatter があります。1.5.8.1 の本体(net10.0。Implem.Pleasanter.csproj)はこのパッケージを参照していないので、csproj に追加します。フィードの組み立ては共通にし、形式の違いは書き出しの段階だけで分岐します。
図を読み込み中…
URL
GET /feed/{guid}.rss # RSS 2.0
GET /feed/{guid}.atom # Atom 1.0形式はクエリ(?format=rss)でも切り替えられますが、フィードリーダーは URL をそのまま識別子に使うことが多いため、拡張子で分けます。ルートは iCal と同じく既存の Default ルートより前に登録します。
endpoints.MapControllerRoute(
name: "FeedRss",
pattern: "feed/{guid}.rss",
defaults: new { Controller = "Feed", Action = "Rss" },
constraints: new { Guid = "[A-Fa-f0-9]{32}" });
endpoints.MapControllerRoute(
name: "FeedAtom",
pattern: "feed/{guid}.atom",
defaults: new { Controller = "Feed", Action = "Atom" },
constraints: new { Guid = "[A-Fa-f0-9]{32}" });設定の保存先
iCal と同じく専用テーブルを推奨します。iCal の列に MaxItems(既定 50 程度、0 は無制限)を足した構成です。
図を読み込み中…
iCal と 1 つのテーブルにまとめ、FeedType(ICal / Rss / Atom)で区別する案もあります。
| 観点 | 統合テーブル | 分離テーブル |
|---|---|---|
| GUID の検索 | 1 テーブルで全形式を引ける | 形式ごとに引き先を分ける |
| 列 | 形式固有の列が NULL 許容になる | 必要な列だけ |
| 新しい形式の追加 | FeedType を足すだけ | テーブルを追加 |
レコードとエントリの対応
| レコード | RSS 2.0 | Atom 1.0 |
|---|---|---|
画面表示タイトル(ItemTitle) | <title> | <title> |
内容(Body) | <description> | <summary> |
レコードの URL(/items/{Id}/edit) | <link> | <link href> |
pleasanter-{SiteId}-{Id} | <guid isPermaLink="false"> | <id> |
| 更新日時 | <pubDate> | <updated> |
| 作成日時 | ― | <published> |
取得はビューの Where を適用し、iCal と違って更新日時の降順に並べ、MaxItems 件で打ち切ります。
図を読み込み中…
private SyndicationItem BuildItem(Context context, SiteSettings ss, FeedSetting setting, DataRow row)
{
var id = row.Long("Id");
var title = setting.TitleStyle == "Custom"
? ReplaceTemplate(context, ss, setting.TitleFormat, row)
: row.String("ItemTitle");
var content = setting.ContentStyle == "Custom"
? ReplaceTemplate(context, ss, setting.ContentFormat, row)
: row.String("Body");
return new SyndicationItem(
title: title,
content: content,
itemAlternateLink: new Uri(Locations.ItemEditAbsoluteUri(context: context, id: id)),
id: $"pleasanter-{ss.SiteId}-{id}",
lastUpdatedTime: new DateTimeOffset(row.DateTime("UpdatedTime").ToLocal(context: context)));
}ReplaceTemplate は ss.IncludedColumns(format) で取り出した列を値で置き換える新設のメソッドです。レコードの URL は、通知の {Url} と同じ Locations.ItemEditAbsoluteUri で作れます(ResultModel.cs)。
キャッシュ制御
フィードリーダーは定期的にポーリングするので、変更が無ければ本文を作らずに済むようにします。
| ヘッダ | 値の例 | 目的 |
|---|---|---|
Cache-Control | public, max-age=300 | 5 分間はキャッシュさせる |
ETag | フィード内容のハッシュ | If-None-Match が一致すれば 304 |
Last-Modified | 最新の更新日時 | If-Modified-Since 以降に変更が無ければ 304 |
iCal との違い
| 観点 | iCal | RSS / Atom |
|---|---|---|
| 形式 | RFC 5545 のテキスト | XML |
| 日時の意味 | 予定の開始・終了 | レコードの更新日時 |
| 終日判定・VTIMEZONE | 必要 | 不要(オフセット付き日時で足りる) |
| 並び・件数 | カレンダー列で絞る | 更新日時の降順、MaxItems で打ち切り |
注意点
| 項目 | 内容 |
|---|---|
| URL の秘密性 | 漏れると誰でも読める。再発行を用意する |
| 実行ユーザーの無効化・ビューの削除 | iCal と同じ扱い |
| HTML の除去 | 内容や説明項目の HTML・Markdown は除去してから出力する |
| 文字コード | XmlWriterSettings.Encoding に UTF-8 を明示する |
MaxItems | 0(無制限)は全件取得になるので負荷に注意 |
| Wiki | ReferenceType が Wikis のサイトは一覧の概念が違うので対象外にする |
| リーダーの互換性 | Atom に対応していないリーダーもある |