AtomでサイトのFeedを取得可能にした
今日はサイトにAtomを実装してみました。これにより、RSSリーダーで更新情報を受け取れるようになりました。いまさらRSS、と思われるかもしれませんが、今現在まさに私がRSSリーダーの有用性を再認識している真っ最中なのです。当サイトのAtomフィードは、https://koyomiji.com/feed.xmlから取得することができます。
少し実装についてのお話をば。最初に、feedというnpmパッケージを見つけました。これはAtomもRSSも出力できるライブラリなのですが、これだとHTMLのエスケープまわりがうまく扱えませんでした。ということで、jsdomを使ってプリミティブにDOM を作成して出力することにしました。Atomを自力で生成している例はあまり見当たりませんでしたが、RFC 4287の日本語訳が非常に役立ちました。これを頼りに、なんとかW3CのFeed Validatorを通るAtomを生成することができました。このValidatorは要素名はもちろん、日付のフォーマットもきちんとチェックしてくれます。
Atomフィードの生成処理はこんな感じ。直接転用できるわけでは無いので、雰囲気だけ感じていただければ。
renderer.use('/feed.xml', (ctx) => {
const atomNS = 'http://www.w3.org/2005/Atom';
const document = window.document.implementation.createDocument(atomNS, 'feed');
const create = newElementCreator(document, atomNS);
const feed = document.firstChild! as Element;
feed.appendChild(create('title', {}, '曆路喫茶館'));
feed.appendChild(create('id', {}, 'urn:uuid:7e260dae-5479-45c2-bad8-0be227c48ab8'));
feed.appendChild(create('link', { rel: 'self', href: 'https://koyomiji.com/feed.xml' }));
feed.appendChild(create('link', { rel: 'alternate', href: 'https://koyomiji.com/' }));
feed.appendChild(create('updated', {}, toISOStringJST(new Date())));
feed.appendChild(create('icon', {}, 'https://koyomiji.com/favicon.ico'));
const author = create('author');
author.appendChild(create('name', {}, 'Komichi'));
author.appendChild(create('email', {}, 'k0michi@koyomi.co'));
feed.appendChild(author);
for (const e of Object.values(model.entries)) {
const entry = create('entry');
entry.appendChild(create('title', {}, e.title));
entry.appendChild(create('summary', {}, e.description));
entry.appendChild(create('id', {}, `urn:uuid:${e.id}`));
entry.appendChild(create('link', { rel: 'alternate', href: new URL(toPathname(e.path), 'https://koyomiji.com/').toString() }));
entry.appendChild(create('published', {}, e.created));
entry.appendChild(create('updated', {}, e.modified));
feed.appendChild(entry);
}
const serializer = new window.XMLSerializer();
return '<?xml version="1.0" encoding="UTF-8"?>\n' + serializer.serializeToString(feed);
});
ちなみに、newElementCreator
というのはこんな関数。
export function newElementCreator(document: Document, namespace: string) {
return (type: string, props: Record<string, string> = {}, children?: string) => {
const $elem = document.createElementNS(namespace, type);
for (const [key, value] of Object.entries(props)) {
$elem.setAttribute(key, value);
}
if (children != null) {
$elem.append(children);
}
return $elem;
};
}
DocumentやElementを作成するときに、namespaceを指定するのがミソです。これが無いと生成後のXMLが正しくなりません。namespaceを指定するために、createElementNS
で各要素を生成していきます。
<id>には、URNと呼ばれる識別子を指定します。直接UUIDを指定するのではなくて、urn:uuid:
をUUIDの先頭にくっつけます。私はこのURNというものを今日初めて知りました。
当然ですがこれらのIDは毎回、同じentryに対して同じIDが維持されていなければなりません。今までそれぞれの投稿にIDを付与していなかったのですが、その必要性が生じたので全ての投稿に付与しておきました。
RSSやAtomは、比較的能動的に更新情報を取りに行く仕組みなので、個人的には好みです。情報の取捨選択がしやすいですし。みんなもRSSリーダーを使おう、という布教です。