はじめに
OBS Studio のプラグインを探せるカタログ「配信プラグイン倉庫くん」を、Cloudflare Workers の上に作りました。
Web の画面は閲覧専用で、Hono の JSX で HTML をサーバーで組み立てて返すだけにしています。
結論として、トップページは Lighthouse のモバイルで Performance 99、デスクトップで 100 になりました。ページを開くたびにサーバーで HTML を作っていますが、応答は約 0.1 秒で返ります。
私は普段 Next.js で Web サイトを作っていますが、それと比べても、ページを移るたびの読み込みがはっきり速く感じました。
また画面の見た目については、あえて 2000 年代前半のソフトウェア紹介サイトのような古風なものにしています。
色を 2 つに絞り、リンクには下線を引き、ボタンは立体的な枠で描いています。いまどきの意匠より要素が少なく、探す人が迷うところを減らしたい意図です。この記事では、どういうことを考えながらこのサイトを作ったか書いていきます。
対象読者
- Cloudflare Workers で小さな Web サービスを作りたい方
- 一覧と詳細を見せるだけのサイトに SPA のフレームワークを使うか迷っている方
検証環境
| 項目 | 内容 |
|---|---|
| 実行環境 | Cloudflare Workers Free、D1、KV |
| Hono | 4.13.17 |
| Wrangler | 4.158.0 |
| 測定 | Lighthouse 13.6.0、HeadlessChrome 157、Windows 11 |
| 測定日 | 2026-11-10 |
配信プラグイン倉庫くんとは
配信プラグイン倉庫くんは、OBS Studio のサードパーティ製プラグインを探せる、非公式で無料のカタログです。プラグインの作者が自分の GitHub リポジトリを登録すると、GitHub Releases の配布物を既知のマルウェアのデータベースと照合し、該当が無ければ掲載します。
主な操作手段は MCP で、Claude Code や Codex などの AI エージェントから検索と登録ができます。Web の画面は、人が一覧と詳細を見るための閲覧専用の入口です。
開発は Claude Code に任せました。私は動く画面を触って気になった点を伝え、色や配置の方針を決める役を担っています。
構成
1 つの Worker が、MCP のエンドポイントと Web の画面の両方を返します。
| 要素 | 採用 | 理由 |
|---|---|---|
| 画面の描画 | Hono の JSX をサーバーで描画する | 閲覧専用で、ブラウザーで状態を持つ必要が無い |
| 検索 |
GET のフォームの送信 |
ページの URL に検索語が残り、そのまま共有できる |
| 言語の切替え |
<details> の中のリンク |
開閉に JavaScript が要らない |
| スタイル | 1 枚の CSS を Worker から配信する | ページの間で使い回せ、キャッシュが効く |
| 画像 | 各プラグインのロゴと説明の画像を、D1 に保存して自分のドメインから配信する | 作者の GitHub に依存せず、大きさと形式を確かめてから出せる |
画面の設計書には、共通の決まりとして「JavaScript を使わない」と書きました。
- 画面には作者が GitHub に書いた文章が並ぶ
- JavaScript の実行を CSP で禁じておけば、悪意のある文字列が紛れ込んでも閲覧者のブラウザーで動かない
- ページの中身がサーバーの返す HTML だけになり、表示の確認を HTML の取得で済ませられる
たとえば「連絡先を表示」のボタンも、JavaScript で表示を切り替えず、フォームの送信でサーバーがアドレス入りのページを返す形にしました。
Hono の JSX で HTML を返す
トップページの処理を抜粋します。D1 からプラグインを検索し、JSX で組み立てた HTML を c.html() で返すだけです。
app.get('/', async (c) => {
const lang = c.get('lang');
const t = DICTS[lang];
const keyword = (c.req.query('q') ?? '').slice(0, 100).trim();
const category = CATEGORIES.find((v) => v === c.req.query('category')) as Category | undefined;
const result = await searchPlugins(c.env, { keyword, category, limit: PAGE_SIZE, offset: 0 });
return c.html(
<Layout meta={/* 省略 */}>
{/* 一覧の描画 */}
</Layout>,
);
});
検索欄は、ただの GET のフォームです。言語の切替えは <details> で開閉します。
<search>
<form method="get" action="/">
<input type="hidden" name="lang" value={lang} />
<input type="search" name="q" value={meta.keyword ?? ''} maxlength={100} />
<SearchButton label={t.searchButton} />
</form>
</search>
<details class="lang">
<summary aria-label={t.languageLabel}>{LANG_NAMES[lang]}</summary>
<ul>
{LANGS.map((l) => (
<li><a href={withLang(meta.path, l)} hreflang={l} lang={l}>{LANG_NAMES[l]}</a></li>
))}
</ul>
</details>
Hono の JSX は、埋め込んだ文字列を自動でエスケープします。GitHub から取ってきた概要の文は作者が書いたものなので、生の HTML としては出さず、すべてこのエスケープを通しています。JavaScript を使わないため、Content-Security-Policy も default-src 'none' を基本にでき、スタイルシートと画像を自サイトに限るだけで済みました。
Hono の JSX は React と同じ書き方で HTML を組み立てられますが、React 自体は使っていません。ブラウザーへ送るのは描画済みの HTML だけなので、ハイドレーションのための JavaScript も発生しません。
古風な画面にした理由
見た目は、2000 年代前半くらいのソフトウェア紹介サイトに合わせました。
| 項目 | 決めたこと |
|---|---|
| 幅 | 本文と右の列を、最大幅 860 px で中央に寄せる |
| 文字 | 基本の大きさは 13 px、見出しは 17 px |
| 色 | テーマカラーは 2 つだけ。カテゴリごとに色を分けない。例外は掲載停止を示す赤だけ |
| 区画 | 1 px の灰色の罫線で囲み、角を丸めない |
| リンク | 下線を付ける |
| ボタン | 上と左を明るく、下と右を暗くした立体的な枠で描く |
| スクロール | 画面全体の縦方向だけ。要素の中のスクロールは使わない |
立体的なボタンは、CSS の border-color の 4 辺の色を変えるだけで描けます。押したときは明暗を入れ替えます。
button {
background: var(--button-bottom) linear-gradient(var(--button-top), var(--button-bottom));
border: 1px solid;
border-color: var(--button-light) var(--button-dark) var(--button-dark) var(--button-light);
border-radius: 2px;
}
button:active {
border-color: var(--button-dark) var(--button-light) var(--button-light) var(--button-dark);
}
この見た目を選んだのは、単に懐かしさのためではありません。
きっかけは、最近よく見る、余白を広く取った洗練されたデザインのページです。見た目は美しい一方で、1 画面に入る情報が少なく、キャッチコピーを読んでも具体的に何ができるのかを知るまでにスクロールが続くことがありました。倉庫くんは、プラグインを探しに来た人が「名前」「何をするものか」「どの OS で使えるか」「どこからダウンロードするか」の情報を一目で確かめられる場所にしたいと考えました。
そこで思い出したのが、昔からあるソフトウェア紹介サイトや Wiki のような画面です。 文字は小さめでも、1 画面に必要な情報が並び、どこを押せば何が起きるかが見た目から分かります。色と部品の種類を絞ったこの作りなら、探す人が迷うところを減らせると考えました。長く使われているということは、それだけ洗練されているということでもあります。
方向性が決まったらあとは「Claude Code が作った画面を見る⇄気になった点をまとめて伝える」の繰り返しでした。たとえば、色を増やすとカテゴリのタグが目立ちすぎるので 2 色に絞る、とか、下線の無いリンクは本文と見分けにくいので下線を付ける、狭い画面では右の列の区画を閉じて見出しで開くといった指摘です。
指摘を場当たり的に直そうとする動きを Claude Code が見せたときは一旦停止させ、先に設計の文書を直してもらってから実装してもらいました。指摘同士が食い違ったり、指摘への対応状況を見失ったりするのを防ぎたかったからです。
測定結果
Lighthouse 13.6.0 で、本番の 2 つのページを測りました。
| 指標 | トップ (モバイル) | トップ (デスクトップ) | 詳細 (モバイル) |
|---|---|---|---|
| Performance | 99 | 100 | 75 |
| FCP | 1.7 秒 | 0.5 秒 | 1.6 秒 |
| LCP | 1.7 秒 | 0.5 秒 | 8.0 秒 |
| TBT | 0 ms | 0 ms | 0 ms |
| CLS | 0 | 0 | 0 |
| サーバーの応答 | 114–117 ms | 92 ms | 117 ms |
| 転送量の合計 | 約 131 KiB | 約 131 KiB | 約 1,351 KiB |
トップのモバイルは 2 回、そのほかは 1 回ずつ測りました。モバイルには、通信と CPU の遅延を模擬する Lighthouse の標準の設定を使っています。
トップページで読み込むのは 9 件で、内訳は次のとおりです。
| 種類 | 件数 | 転送量 |
|---|---|---|
| HTML | 1 | 約 5 KB |
| CSS | 1 | 約 4.7 KB |
| 自前の JavaScript | 0 | 0 |
| 画像 | 5 | 約 125 KB |
転送量のほとんどは、プラグインのロゴの画像です。HTML と CSS を合わせても約 10 KB で、TBT が 0 ms なのは、ブラウザーで実行する処理がほぼ無いためです。
詳細ページの LCP が 8.0 秒なのは、作者が置いた約 1.3 MB の説明の GIF が LCP の対象になったためです。HTML と CSS の大きさはトップと変わりません。GIF の最初のコマを静止画として先に出し、GIF は利用者が開いたときに読み込む形にすると改善できそうです。(※ まだ試していません。)
感想
今回の速さと分かりやすさは、どちらも画面に置くものを減らす判断から来ていると考えています。
JavaScript を送らないと決めたことで、検索はフォーム、言語の切替えは <details> という、ブラウザーに最初からある部品で作ることになりました。
古風な見た目にしたことで、色や部品の種類も減りました。足すものを選ぶのではなく、置かないものを先に決めたことが、結果として良かったのだと思います。
Next.js のようなフレームワークは、画面の中で状態を持ち、操作に応じて表示を変えるサービスでこそ力を発揮します。今回のように一覧と詳細を見せるだけのサイトでは、その仕組みを使わずに済むので、HTML を返すだけの構成があっていたのでしょう。作るものに合わせて道具を選ぶのが大事だと感じています。
一方で、詳細ページの GIF のように、作者が置く素材は自分では減らせません。閲覧専用のサイトでも、外から入ってくる画像の扱いは別に設計しておく必要がありそうです。
まとめ
配信プラグイン倉庫くんの Web カタログを、Hono の JSX で HTML を返すだけの構成と、2000 年代前半のソフトウェア紹介サイトのような画面で作りました。トップページは、自前の JavaScript を送らずに Lighthouse のモバイルで 99 になりました。
閲覧が主のサイトを作るときは、まず「スクリプトを使わない」と決めてから、フォームとリンクでどこまでできるか試してみるのをおすすめします。
