ディレクトリサイトの構築は、一見簡単そうに思えます。しかし、単なる精選された目次を表示するためだけに、データベース、キャッシュレイヤー、そしてリアクティブなフロントエンドフレームワークを連携させる事態に陥ると、途端に難易度が上がります。私は最近、ソーシャルメディア・ソフトウェアを比較するためのサイト「Social Tools List」を構築しました。私の目標は、サイトを迅速にオンラインにし、高速に保ち、ツールを追加または更新する時以外はインフラのメンテナンスを避けたいということでした。そこで、サイト生成にAstro、構造化データにTypeScript、デプロイにCloudflare Workersを採用する「スタティック・ファースト(静的優先)」なスタックを選択しました。その結果、瞬時にロードされ、ホスティングコストがほぼかからず、データベース管理も不要なサイトが完成しました。

ディレクトリにおいて、なぜスタティック・ファーストが理にかなっているのか

多くのウェブアプリは、安全でモダンな選択肢だという理由で、サーバーサイドレンダリングやシングルページアーキテクチャをデフォルトとして採用しています。しかし、すべてのサイトがリクエストのたびに動的なユーザー入力を受け取るわけではありません。Social Tools Listは、読み取り中心のリソースです。比較データが更新されるのは、訪問者がページを更新したときではなく、私がアップデートをプッシュしたときです。事前にHTMLをレンダリングしておくことで、エッジでのデータベースクエリ、オンザフライのテンプレートコンパイル、あるいはブラウザでのハイドレーションによるオーバーヘッドを排除できます。ビルド時にサイトを生成して静的ファイルをデプロイし、軽量なWorkerにラッパー(外枠)の処理を任せています。これにより、レスポンスタイムを低く抑え、ランタイムエラーの大部分を排除できています。

データベースではなく、TypeScriptにデータを保存する

私はデータベースを使用していません。ディレクトリ内のすべてのツールは、slug、name、domain、およびサポートされているワークフローの配列を持つTypeScriptオブジェクトとして定義されています。典型的なエントリは以下の通りです。

{
  slug: 'buffer',
  name: 'Buffer',
  domain: 'buffer.com',
  workflows: ['scheduling', 'analytics']
}

サイトを構築するのと同じ言語でデータを保存することには、2つの即座のメリットがあります。第一に、プルリクエストがそのままコンテンツレビューになります。ツールを追加すると、diff(差分)によって正確なフィールドと値が表示されるため、チームメイトはCMSのインターフェースを覚えることなく、タイポや誤ったドメインに気づくことができます。第二に、TypeScriptコンパイラがすべてのレコードの型を強制します。もしslugの入れ忘れやワークフローキーのスペルミスがあれば、不正なデータがページに到達する前にビルドが失敗します。

データベースを導入すると、マイグレーション、接続文字列、キャッシュ戦略、バックアップルーチンが必要になります。手動で管理している数百件程度のディレクトリにとって、そのオーバーヘッドは単なる足かせです。TypeScriptモジュール内の静的データは、このモデルにおいて最も安価で「正しい」選択です。ここで言う「正しい」という部分は重要です。単にコストを節約するだけでなく、存在しない問題を解決するための抽象化レイヤーを取り除くことが目的なのです。

ルーティングとレンダリングはAstroに任せる

Astroは、単一のダイナミックルートからツールごとに1つのHTMLページを生成します。1つのレイアウトコンポーネントを定義するだけで、Astroがすべてのエントリに対してメタデータ、見出し、構造化データを自動的に生成します。同じデータセットがメインのインデックス、ワークフローカテゴリのハブ、および個別の詳細ページを駆動しているため、ホームページのカードに詳細ページとは異なる説明が表示されることはありません。従来のCMS構成では、APIが返すバージョン、キャッシュのバージョン、クライアントサイドでのレンダリング結果がそれぞれ異なるという「乖離(drift)」がよく起こります。単一の信頼できる情報源(single source of truth)からの静的生成は、それを防いでくれます。

また、Astroのアイランドアーキテクチャ(island architecture)を使えば、ページ全体をJavaScriptで汚染することなく、小さなインタラクティブな要素を簡単に追加できます。サイトは静的HTMLとして配信され、フィルタリング用のスクリプトだけがDOMの特定の箇所をハイドレートします。ドキュメント全体を包み込むようなフレームワークのランタイムは存在しません。さらに、Astroはページのメタデータを第一級の関心事として扱います。各ツールのページには、TypeScriptのレコードから直接導出された独自のtitleタグとmeta descriptionが付与されるため、個別のプラグインやhead管理ライブラリは必要ありません。

フレームワークを使わないフィルタリング

検索やフィルタリング機能があると、開発者はReactやVue、あるいは重い状態管理ライブラリをインストールしたくなります。私はそれに抗いました。Astroはサーバー上でリスト全体をプレーンなHTMLとしてレンダリングします。1キロバイトに満たない小さなバニラJavaScriptスクリプトがブラウザで動作し、ワークフロータグやテキストの一致に基づいてリスト項目のdisplayプロパティを切り替えるだけで十分なのです。

API主導の開発に慣れている人にとって、マークアップ全体を配信するのは非効率に聞こえるかもしれません。しかし、一般的な動的アプローチのオーバーヘッドを考えてみてください。ブラウザがJavaScriptのバンドルをダウンロードし、コンポーネントツリーをハイドレーションし、エンドポイントを呼び出し、JSONを待機してから、行をレンダリングします。100件未満のツールをリストするディレクトリであれば、その一連の手順は、ドキュメント内に既に存在するdivを非表示にするよりも遅く、信頼性も低くなります。私のスクリプトは、フィルターボタンにイベントリスナーをアタッチし、各行のdata-workflow属性を読み取り、一致しないものをhiddenに設定します。この操作は数ミリ秒で完了します。

リストは初期HTMLに含まれているため、JavaScriptなしでもサイトを利用できます。検索エンジンのクローラーは、すべてのリンクとすべての説明を確認できます。低速なネットワーク環境のユーザーやスクリプトブロッカーを使用しているユーザーも、ディレクトリの全内容にアクセスできます。フィルタリングは機能の拡張(enhancement)であり、利用の障壁(gate)ではありません。

コードとしてのサイトマップとRobots

サイトマップやrobots.txtは、手書きで後から追加するものではありません。これらは、同じデータセットとURLヘルパーを使用するAstroのルートです