okayurisotto.net

私が好きでやったことが他の人のためにもなったらお得かも!

Cloudflare WorkersでFirefoxのリリースノートのAtomフィードを作る

公開日:
最終更新日:

この記事は、Cloudflare Advent Calendar 2025の7日目の記事です!

はじめに

Firefoxのリリース情報をフィードとして購読したかったのですが、残念ながら公式からはRSSやAtomは提供されていないようでした。しかし偶然、最新のリリースノートへリダイレクトする固定のURLが、FirefoxのWebサイトで使われているのを発見しました。このURLへHEADリクエストを送信し、レスポンスのLocationヘッダーを見れば、最新のリリースノートのURLを取得できるはずです。というわけで、そのようにして取得したURLをAtomに変換しR2で配信するCloudflare Workersを作ってみました。

注意点

他所様のWebサーバにプログラムからアクセスする際には、/robots.txtや利用規約を遵守するのは当然として、サーバに負荷をかけすぎることがないよう細心の注意を払って実装しましょう。注意を払っていても、Librahack事件のように事件として取り扱われる事態を招くことはあります。注意を払い過ぎるくらいの心持ちでいるべきかと考えます。

この記事では、「フィードを提供していないWebサイトのフィードを作る方法」としてCloudflare Workersでの開発を解説しますが、この記事は、フィードを提供していないWebサイトをモラルなくスクレイピングすることを奨励するものではありません。

Cloudflare Workersについて

Cloudflare Workersは、Cloudflareの提供するサーバレスコンピューティングサービス(基本無料)です。TypeScriptで書いたプログラムを、Cloudflareのサーバ上で実行できます。今回の場合は、TypeScriptでHTTPサーバを作り、作ったそれがCloudflareのサーバ上で実行される形です。(ドメインとしては、fxfeed.okayurisotto.workers.devのようなものが自動的に与えられます。)

Cloudflareは全世界にデータセンターを所有しており、スケーリングは自動で行われ、デプロイでは最もパフォーマンスが高くなる場所に自動的に配置されるなどの最適化が行われます。私達がその詳細なデプロイ方法などを制御する必要はありません。自動です。

デプロイをはじめとする操作は、基本的にはwranglerというコマンドラインツールから行います。wrangler devで開発サーバが立ち上がり、wrangler deployでCloudflareのネットワークへのデプロイが行われます。(事前に設定しておけば、GitHubのリポジトリのmainブランチへのpushで、自動的にデプロイされるようにもできます。)

Cloudflare WorkersのJavaScriptランタイムでは、Node.js同様、JavaScriptエンジンとしてV8を使用していますが、Node.jsと全く同じというわけではありません。そのため、npmにあるNode.js向けのパッケージが使えないことや、使う上で設定が必要なことがあります。

とはいえ基本的なHTTPサーバとしての機能は、外部のパッケージをインストールすることなく実装できるようになっています。(今回作ったサーバも、外部依存はありません。)

Cloudflare Workersの基本的な書き方については、Cloudflareの公開する開発者向けドキュメントの例を参考にしてください。

実装1:Firefoxの公式サイトをfetchしてみる

Cloudflare Workers内でもfetch APIは利用可能です。次のようにして使えます。

const response = await fetch("https://www.firefox.com/en-US/firefox/notes/", {
  method: "HEAD",
  redirect: "manual",
});

const location: string | null = response.headers.get("Location");

console.log(location); // e.g. "/en-US/firefox/144.0.2/releasenotes/"

今回は、最新のリリースノートへリダイレクトする固定のURLをうまく利用することで、最新のリリースノートを取得します。ですので、HEADリクエストでレスポンスヘッダのLocationを読む形で実装します。

HEADリクエストはレスポンスにbodyがあることを求めないため、軽量かつ低負荷です。FirefoxのWebサイトへの負荷を減らせるだけでなく、Cloudflare Workersの応答を高速化することにも寄与しているはずです。ただし注意点として、fetch APIはデフォルトでは自動的にリダイレクトを辿ってしまい、Locationレスポンスヘッダーが読めないため、redirect"manual"に変更する必要があります。

記事執筆現在、得られるURLを絶対パスに変換すると、次のようになっています。

https://www.firefox.com/en-US/firefox/144.0.2/releasenotes/

Atomフィードを作成するにあたり、URL以外にもバージョン情報やその公開日時が得られると便利です。バージョン情報についてはURLに埋め込まれているため、それを読み取ればよいでしょう。URLPatternを使います。

const urlPattern = new URLPattern({ pathname: "/en-US/firefox/:version/releasenotes/" });
const version: string | undefined = urlPattern.exec(url)?.pathname.groups["version"];

console.log(version); // e.g. "144.0.2"

問題は、公開日時の情報をどう手に入れるかです。残念ながら、得られたURLへHEADリクエストを送信したとしても、Dateレスポンスヘッダーの値は、アクセス時点のタイムスタンプになってしまっています。公開日時を得るには、得られたURLへGETリクエストを送信し、レスポンスボディのHTMLをパースし、p.c-release-date要素の中身を読み取る必要があります。

技術的に不可能ではありませんが、そこまでして公開日時を取得したいかという疑問が生まれます。今回作成する予定のAtomフィードは、エントリを最新の1つしか持たない最小限なものです。「アグリゲーターが取得したAtomフィード内の複数のエントリを公開日時で並び替える」といった場面は発生しません。このCloudflare Workersは、リダイレクト情報をあたかもAtomフィードのように取り繕うだけです。

というわけで、公開日時の情報までは無理して取得しないことにします。

実装2:レスポンスを返してみる

/src/index.tsdefault exportしているオブジェクトのうち、async fetch(): {}というのが、HTTPサーバのハンドラです。https://fxfeed.okayurisotto.workers.dev/のようなURLへのアクセスで、この部分が呼び出されます。

export default {
  async fetch(request, env, ctx): Promise<Response> {
    if (request.method !== "GET") {
      return new Response("GETメソッド以外禁止です!", { status: 405 });
    }

    const pathname = new URL(request.url).pathname;
    console.log(`${pathname}にアクセスされました!`);

    return new Response("こんにちは、世界!");
  }
} satisfies ExportedHandler<Env>;

この部分に、先程書いた、FirefoxのサーバへのHEADリクエストを行う処理を書けば良いということです。特に難しいことはありません。

return Response.json({ location, version });

実装3:Atomとしてレスポンスを返してみる

アグリゲーターがレスポンスを処理できるようにするには、JSONではなくXML(application/atom+xml)でレスポンスを返す必要があります。npmにはすでにフィード生成ライブラリがいくつかありますが、今回は文字列を手動で組み上げてXMLとしたいと思います。ライブラリを使おうとすると、公開日時のような、必須な、けれど今回は取得できていないデータを求められてしまい、勝手が悪いためです。

import { env } from "cloudflare:workers";

const GENERATOR = env.GENERATOR; // 環境変数から取得

type Entry = {
  id: string;
  link: string;
  title: string;
};

const generateAtom = ({ id, title, link, entries }: { id: string; title: string; link: string; entries: Entry[] }): string => {
  return (
    `<?xml version="1.0" encoding="utf-8"?>` +
    `<feed xmlns="http://www.w3.org/2005/Atom">` +
    `<id>${this.id}</id>` +
    `<title>${this.title}</title>` +
    `<link href="${this.link}"/>` +
    `<generator>${GENERATOR}</generator>` +
    // `<updated>${opts.updated}</updated>` +
    // `<author>` +
    // `<name>${opts.authorName}</name>` +
    // `</author>` +
    entries.map((entry) => (
      `<entry>` +
      `<id>${entry.id}</id>` +
      `<link href="${entry.link}"/>` +
      `<title>${entry.title}</title>` +
      // `<updated>${entry.updated}</updated>` +
      `</entry>`
    )).join("") +
    `</feed>`
  );
};

見て分かる通り、必要最小限すぎてAtomとして不正なものになってしまっています。atom:feedatom:entryatom:updated要素を持たなければなりませんし、atom:authorも必要です。しかしこればっかりはどうしようもないため、適当な値をでっち上げることも視野に、今度の課題としたいと思います。

実装4:index.htmlを作る

フィードを公開するにあたり、link[rel="alternate"]によってフィードの存在をアグリゲーターへ伝えるHTMLファイルがあると便利です。

const getFallback = (status: number): Response => {
  return new Response(
    `<!DOCTYPE html>` +
    `<title>Unofficial Firefox Release Notes Feed</title>` +
    `<link rel="alternate" type="application/atom+xml" href="/atom" title="Firefox Release Notes">`
    `<h1>Unofficial Firefox Release Notes Feed</h1>` +
    `<h2>Atom</h2>` +
    `<ul>` +
    `<li><a href="/atom">Firefox Release Notes</a>`
    `</ul>`,
    {
      status: status,
      headers: {
        "Content-Type": "text/html",
      },
    },
  );
};

とりあえずこんなものでよいでしょう。

実装5:Cloudflare R2を活用するようにする

ここまでの実装でも十分Atomフィードの配信はできるため、大きな問題はありません。fetchの呼び出しでも適切にキャッシュが使われるようにすれば、高頻度でこのWorkerが呼び出されても、Mozillaのサーバに大きな負荷をかけてしまうことはない……はずです。

しかしそもそもの話として、Cloudflare Workersの無料版は1日に呼び出せる回数が決まっています。その数、1日あたり10万件。RSSやAtomはポーリングを基本とした仕組みですので、この呼び出し回数の上限という概念とは……相性が悪いと言わざるを得ません。

私が使っているフィードリーダー、Feedbroのデフォルトのポーリング間隔は30分。Feedbroをインストールしているデバイスは2台。つまり最大1日あたり1440回。まだまだ余裕はありますが、こうして技術記事としてこのAtomフィードの存在を公にすることを考えたら、少し気にしたほうが良いかもしれません。

そこで、「Cloudflare R2でAtomを配信し、Cloudflare Workerは定期的にR2へAtomを書き出す」というように実装し直してみることにします。

Cloudflare R2とは、Cloudflareが提供するオブジェクトストレージで、AWSのS3のようなものです。R2の非常に良いところは、容量5GBまでは無料で使えるところと、エグレス料金が無料なところ。つまり、今回のようにごく小さなAtomファイルを置いておき配信する分には、お金がかかることはないということです。

実装し直すと言っても、難しいことはありません。wrangler r2 bucket create <name>コマンドを実行することで、Cloudflare WorkersでR2が使えるようになります。wrangler.jsonctriggers.cronsを書くことで、そのWorkerのscheduledハンドラに書いた処理が定期的に実行されるようになります。あとは、R2に書き出すような処理をscheduledに書くだけです。

ただし注意点として、Cloudflare R2のサーバはindex.htmlを優先的に表示するようにはなっていません。つまり、https://r2.example.com/index.htmlへのアクセスではindex.htmlが取得できたとしても、https://r2.example.com/へのアクセスは404になってしまいます。これに対処するには、URL書き換えルールを作成する必要があります。

今回、私はすでに所有しているokayurisotto.netのサブドメイン、fxfeed.okayurisotto.netをR2バケットのカスタムドメインとして設定することにしました。よって、次のような式に一致する受信リクエストに対するURL書き換えルールを設定しました。

(http.host eq "fxfeed.okayurisotto.net" and ends_with(http.request.uri.path, "/"))

実行内容はDynamicのconcat(http.request.uri.path, "index.html")です。

こちらのコミュニティの投稿を参考にしました:

おわりに

この記事では極力単純化したコードの断片を紹介してきましたが、実際に作ったものでは、安定版Firefox以外にも、Beta版やNightly版、AndroidやiOSといった別プラットフォーム向けのFirefoxのリリースノートのフィードも公開するようにしていて、抽象化の必要があったため、もう少し複雑です。

フィードは、他の人にとっても役立つものかもしれないという期待から、fxfeed.okayurisotto.netにて公開されています。Mozillaとは一切関係がなく、またMozillaの承認を受けてもいません。自己責任でご利用ください。もしこのフィードが役に立ったようであれば、Mozillaにリリースノートフィードを公式で提供するよう提案していただければ幸いです。