AtomでサイトのFeedを取得可能にした

#56
2022.9.14

今日はサイトに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リーダーを使おう、という布教です。