本文へスキップ
Website Boilerplate

@tuqulore-inc/eleventy-plugin-preact

本ページは、JSX / TSX / MDX を Eleventy のテンプレートとして扱う @tuqulore-inc/eleventy-plugin-preact の内部設計を扱います。API リファレンスとインストール手順はパッケージの README を参照してください。日常の書き方は Preset の規約 / テンプレート を参照してください。

何を SSR しているか

本プラグインは、Eleventy のテンプレート形式として .jsx / .tsx / .mdx を登録し、そのすべてを Preact のサーバサイドレンダラで HTML に変換します。JSX / TSX / MDX はいずれも Node.js のカスタムローダを通してモジュールとして読み込まれるため、テンプレートファイル自体を通常の import と同じ規則で扱えます。.ts はテンプレートとしては登録しませんが、テンプレートやコンポーネントから import する対象として同じローダで解決します。

サーバ側で完結する処理はここまでです。クライアント側の JavaScript は Partial Hydration の層 (eleventy-plugin-preact-island) が担い、本プラグイン自体は「HTML を返すだけ」の関心に閉じます。

なぜ Preact なのか

本ボイラープレートは、SSR と Partial Hydration の両方で同じ UI ランタイムを使う設計を選んでいます。候補として React / Preact / Solid / Svelte がありましたが、次の理由で Preact を採用しています。

  • 単独の bundle が小さく、esm.sh などの CDN で配布できます。クライアント側は import map で preact を解決し、bundle サイズを最小化できます。
  • React 互換 API を持ち、JSX 記法や useState / useEffect などのフックがそのまま使えます。
  • SSR は preact-render-to-string で完結し、追加のランタイムサーバは要りません。
  • @11ty/is-land の初期化フックに接続しやすく、Partial Hydration の実装が薄く済みます。

別ランタイムへ差し替える要求が出た場合は、preset ごと差し替える (フォークする) 想定です。1 つの preset に複数ランタイムを併存させる構造は取りません。

なぜ MDX を選んだのか

ページ本体を書くフォーマットに Markdown だけを採用すると、動的なコンポーネント (Island) を差し込むために別ファイル・別テンプレート言語が必要になります。逆に JSX だけを採用すると、記事の本文で見出しや箇条書きを書くのが冗長になります。

MDX は Markdown 8 割、JSX 2 割のバランスを 1 ファイルで表現できます。docs、記事、ランディングページのいずれでも同じ書き方で通せる点を優先しました。

トランスフォーム層に Rust を選ぶ

Preact と MDX という UI 側の選択を決めた後、それらを Node.js から読み込む変換層に何を置くかが残ります。本プラグインは、JSX / TSX / TS のトランスパイルに oxc-transform を、MDX のコンパイルに Sätteri を採用しています。どちらも Rust 実装で、napi 経由のネイティブ束縛として Node.js から呼び出せます。

JSX / TSX / TS 側の変換は、以前は @babel/core@babel/preset-react に任せていました。Babel は依存パッケージが多く、また 1 ファイルずつ順に変換する SSR ローダの用途では速度が伸びません。oxc-transform はプロセス境界を越えない関数呼び出しで完結し、同じ用途で数十倍高速に動きます。JSX ランタイムを runtime: "automatic", importSource: "preact" に固定し、プラグイン側で常に同じオプションを渡します。ここは Eleventy 公式が推奨する tsx ラッパ (中身は esbuild) を採らなかった理由でもあります。tsx は利用者の tsconfig.json を読み、allowJs の設定次第で .jsx.tsx の JSX ランタイムが食い違うことがあります。プラグイン側でオプションを固定すれば、この不整合は構造的に発生しません。

MDX 側は、以前は @mdx-js/node-loader を経由して @mdx-js/mdx に GFM / frontmatter / heading anchor などの remark プラグインを積んで実現していました。unified のパイプラインはプラグインごとに構文木を全走査するため、プラグインが増えるほど直線的に遅くなります。Sätteri は Rust 側で構文木を一度だけ作り、JS プラグインには「タグ名でフィルタ済みのノード」だけを渡す設計です。同じ機能セットで比較して MDX コンパイルの実測が桁違いに速くなり、remark-gfm / remark-frontmatter / remark-mdx-frontmatter / rehype-slug / rehype-autolink-headings を積み上げる必要も消えます。GFM と YAML frontmatter は組み込みで有効になり、見出しの id 付与だけ本プラグインが薄い HAST プラグインを持ちます。

この置き換えの結果、.tsx / .ts を SSR で扱えるようになり、.client.tsx はクライアントで動くが SSR では読めないという拡張子の非対称を解消できました。.jsx / .tsx / .ts / .mdx はすべて同じローダ経路に乗ります。MDX ページのページデータは YAML frontmatter で渡す規約に切り替え、Sätteri が組み込みで扱う frontmatter block をそのまま利用します。.jsx / .tsx は JSX 構文の都合で --- を書けないため、こちらは従来通り export const data = { ... } を使います。詳細は Preset の規約 / テンプレート に整理しています。

eleventy シングルトンの提供

本プラグインは、SSR 中に有効な eleventy シングルトンを提供します。テンプレートや partial から props を渡し続けなくても、import するだけでページデータやグローバルデータに触れます。

シングルトンの中身と使い方は Preset の規約 / Data Access を参照してください。実装としては、Eleventy がテンプレートを描画するたびにシングルトンを差し替える方式を取っており、非同期並列描画のケースでも一貫性が保たれるように設計されています。

Layout Chaining の実装

Eleventy の Layout Chaining は、本プラグインが MDX のローダを通して透過的にサポートします。子テンプレートの描画結果を親テンプレートの eleventy.content として受け渡す仕組みで、親側は dangerouslySetInnerHTML で HTML を差し込みます。

MDX と JSX の書き分け、ページデータの書き方は Preset の規約 / テンプレート にまとめてあります。

単体で使いたいときは

@tuqulore-inc/eleventy-plugin-preact は、プリセットを介さずに単体でも導入できます。既存の Eleventy プロジェクトに JSX / MDX の SSR だけを足したい場合、この選択肢を取れます。

その場合は Partial Hydration が付いてこないため、<Island> を使うと解決に失敗します。Island を併用したいときは @tuqulore-inc/eleventy-plugin-preact-island を一緒に登録するか、プリセット経由で使うのが早いです。詳細はパッケージの README を参照してください。