Komesanというはてなブックマーク、Twitter、HackerNewsをまとめて表示するサイトを作りました。

ブックマークレットで実行する場合は、次のようなURLのブックマークを利用します。サイトの下部に同じものがおいてあります。

javascript:void(window.open("https://komesan.pages.dev/?url="+encodeURIComponent(location.href)))

自分用のツールでなので、iframe表示用の変な機能もありますが、READMEとか読んでください。

元のデータは、元サイトのAPIを使って取得しているだけです。

作った理由

同様の機能を持つサイトとして http://komepon.net がありますが、HTTPSに対応してなかったのが気になりました。 あと、Remixを使って何か作ってみたくて、検索結果を表示するのは題材としてちょうど良さそうでした。

Remix

Komesanは、Cloudflare PagesRemixを使って動いています。

KomesanのやっていることはAPIの結果を加工して表示するという感じです。 Remixの題材としてちょうど良さそうなので、Remixを使ってみました。 Remixについては作者による記事を見るのがよいと思います。

Remixのphilosophyにあるように、Komesanも特に意識せずに基本的な機能はJavaScriptが無効でも動いているようでした。

RemixはCloudflare Workers/Cloudflare Pagesにデプロイできるので、Cloudflare Pagesでサーバを動かすフレームワークとして便利だと思います。(Next.jsだと結構面倒です。)

Web Standards, HTTP, and HTMLを基本としてるのもあって、Cloudflare WorkersのようなService WorkerライクなAPIのみがある場所で使うのに適したデザインだと思います。

ルーティングは、Next.jsなどのフレームワークとあまり変わりません。

データの読み込みは、loaderという関数を定義するとサーバサイドでデータを取得してReactに渡すPropsのデータを作って渡します。 初回のレンダリングはサーバサイドレンダリングで、Formでのデータ送信、Linkでのページ移動時は、PropsのデータだけをFetchしてPA的な遷移ができます。

データの書き込みは少し変わっていて、Formを使ったPOSTが基本となっています。 <Form>コンポーネントというform要素のラッパーが用意されているので、これを使ってサーバにデータを送信します。

サーバではactionという関数を定義すると、Formのから送信されたデータが受け取れます。 受け取るデータもウェブ標準のFormDataとなっています(これによってJSが無効でも動く)。

全体的に特定の名前でexportしたら、その機能となるようなデザインです。

export const loader: LoaderFunction = () => {
  return [{ name: "Pants" }, { name: "Jacket" }];
};

export const action: ActionFunction = ({ request }) => {
  const body = await request.formData();
  const project = await createProject(body);
  redirect(`/projects/${project.id}`);
}
export default function Products() {
  return <div>App</div>
}

全体的に明示的に型をつけないと型がつかない感じな気はしました(この辺はNext.jsもあんまり変わらない印象)。 バリデーションは、サンプルコードでtiny-invariantが使われてるぐらいで、ユーザー側でやる感じです。

基本的には覚えることが少ないようなデザインになっていそうでした。 今回のKomesanは一種の検索ページというRemixのユースケースとしてぴったりな感じだったからかもしれません。

JavaScriptとFetchを使った通信が色々発生するアプリケーションの場合は、usefetcheruseTransitionを使う必要があったりして、Remix特有の事柄が増えそうな感じはしました。

Cloudflare Pages

RemixはCloudflare Pagesに対応しています。 最初のテンプレート選択でCloudflare Pagesを選べるので、それを選んでCloudflare Pagesにデプロイします。

npx create-remix@latest

Cloudflare Pages側のドキュメントに従って、生成されたコードをもつGitHubリポジトリと接続するだけでOKです。

ローカル開発の場合は、絶賛開発中のwrangler2のalphaが使われているので、ちょっとはまりどころがありました。

wrangler2はMiniflareが元となった、ローカルでのエミュレート機能が入っています。

ローカルでCloudflare Workers KVをエミュレートするには--kvという引数を使います。

  "dev:wrangler": "wrangler pages dev ./public --watch ./build --kv NAMESPACE --binding $(cat .env)",

また、.envファイルが読み込める風のドキュメントになっていますが、Cloudflare Pagesの場合は.envは読み込まれないので--bindingという引数で環境変数を渡してあげる必要があります。

この辺は、成熟したら解決してくるかなという感じはします。

パフォーマンス

単純なページなので、特別なことはありませんでした。

Lighthouse

サーバサイドレンダリング時に、はてなブックマークとTwitterのAPIを叩いているので、その待ち時間がそのままTTFBに反映されます。 Hacker NewsのAPIは、あんまり速くないのでオプトインにしました。 はてなブックマークは、ブックマーク数に応じて重くなるので、ブックマーク数が多いページは表示が遅いです。

HatenaBookMark: 81ms
Twitter: 276ms
HackerNews: 998ms

リアルタイム性は必要ない(元ページを見ればいいだけなので)と思ったので、60秒ぐらいのキャッシュをしています。

まとめ

PCとかモバイルとかどこでも使えるコメント表示するやつが欲しくなったので、Komesanを作りました。

KomesanCloudflare PagesRemixを使って動いています。

ソースコードも公開してるので、Forkして、Twitter v2のAppを作ってTWITTER_TOKENという環境変数にBearer Tokenを設定すれば、自分用のkomesanが動かせます。

RemixはCloudflare Pages/Workers向けのフレームワークという視点でみると便利だなーと思いました。 Nuxt 3もCloudflare Workersとしてデプロイできたり、その方向が強くなっていきそうな気がしました。

あと、Cloudflare Workers向けのフレームワークであるworktopを作ってたLuke EdwardsがCloudflareの人になってたりしていました。 (polkaとかklonaとかファイルサイズが小さいモジュールをよく作っている人)

CloudflareがLincを買収してたので、Cloudflare側が何かアプリケーションフレームワークを提供するのかなーと考えていました。

Remixなどを見てると、思っていたより速く、別のものがCloudflare Workersのフレームワークとして使えるようになっていきそうでした。

同じくService Workers的なAPIを持つDeno Deployとかでも動くようになっていくので、やっとアプリケーションを普通に書いてEdgeにデプロイできるようになっていきそうです。